#35 HLS
もう、動画はスマホで見ることが多くなりました。無線通信で高画質の動画を配信するためにはそれなりの工夫が必要です。加えて、コンテンツを不正な利用から守るためには暗号化などによって保護しなければいけません。以前、DRMについて紹介しましたが、Appleによって開発されたDRMの実装がHLSです。
とはいえ、DRMは不完全な技術といえます。その仕組みに少し精通していれば、ダウンロードも可能です。
検証
Python3.8環境で検証してみます。
$ python3 --version
Python 3.8.2
HLS動画の情報は、全てm3u8というプレイリストファイルにまとめられています。Pythonからm3u8を扱うため、必要なライブラリをインストールします。
$ pip3 install m3u8
HLS動画は、セグメントと呼ばれる細切れの動画ファイルから成り立っています。セグメントを順番にダウンロードして再生することで、通信を最小限に抑え、かつ高速に再生ができます。これらは暗号化されており、単体では再生できません。
もちろん、再生する際には復号しなければいけないので、復号方法もm3u8に書き込まれています。
つまり、m3u8の情報に従えば、HLS動画であれmp4のような通常の動画ファイルとして保存できてしまいます。
以下は、デモ用のスクリプトです。
※当然、悪用は厳禁です。
import os
import re
import m3u8
import urllib
from urllib.parse import urlparse
import string
import random
import glob
import ffmpeg
tmp_dir = "./tmp"
donwload_dir = "./download"
def get_initializer(path):
playlist = m3u8.load(path)
segmap = playlist.segment_map
for m in segmap:
return m
return None
def get_segments(path):
result = []
playlist = m3u8.load(path)
segments = playlist.segments
initializer = get_initializer(path)
result.append(initializer)
for seg in segments:
result.append(seg)
return result
def download_file(url, path, headers=None):
if not headers:
headers = {
"User-Agent": "agent",
}
request = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(request) as web_file:
data = web_file.read()
with open(path, mode='wb') as local_file:
local_file.write(data)
return True
except urllib.error.URLError as e:
print(e)
return False
def concat(path, seg_type="ts"):
files = glob.glob(tmp_dir + "/*")
p = re.compile(r'\d+')
files.sort(key=lambda s: int(p.search(s).group()))
sp = split(files)
fgs = []
for f in sp:
r = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8))
tmp_file = "./tmp/" + r + "." + seg_type
fgs.append(tmp_file)
ffmpeg.input("concat:" + "|".join(f)).output(tmp_file, c="copy").run()
print(fgs)
ffmpeg.input("concat:" + "|".join(fgs)).output(path, c="copy").run()
def split(list):
lists = []
n = 100
for l in range(0, len(list), n):
lists.append(list[l:l+n])
return lists
def download_segments(url, path, headers=None):
if not headers:
headers = {
"User-Agent": "agent",
}
p = urlparse(url)
host = p.scheme + "://" + p.netloc
name = "file"
playlist_path = tmp_dir + "/" + name + ".m3u8"
res = download_file(url, playlist_path)
get_initializer(playlist_path)
if not res:
return False
segments = get_segments(playlist_path)
cnt = 0
try:
for seg in segments:
cnt += 1
file = host + seg.uri
save_path = tmp_dir + "/" + str(cnt)
res = download_file(file, save_path, headers)
if not res:
break
os.remove(playlist_path)
concat(path)
except Exception:
return False
return True
if __name__ == "__main__":
url = ""
path = "./download/out.mp4"
if not os.path.exists(tmp_dir):
try:
os.makedirs(tmp_dir)
except FileExistsError:
pass
if not os.path.exists(donwload_dir):
try:
os.makedirs(donwload_dir)
except FileExistsError:
pass
print("[?] Downloading from " + url)
res = download_segments(url, path)
if res:
print("[+] Success!")
else:
print("[-] Failed")
for f in glob.glob(tmp_dir + "/*"):
os.remove(f)
簡単に解説すると、
①m3u8を読み込み
②必要なファイルを全てダウンロード
③ファイルをmp4に結合
これだけです。
実際に試す場合は、url変数にm3u8ファイルのURLを指定してください。自己責任でお願いします。HLSはAppleの技術なので、MacOS、iOSのみ対応しています。
まとめ
動画のようなコンテンツは、ありふれたものになりつつあります。しかし、悪用に対する防衛方法は不完全なものです。コンテンツ配信を主業とする企業や個人にとってはまさに生死を分かつ問題でしょう。技術の力でなんとかならないかと日々悶々としています。
EOF