見出し画像

#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


いいなと思ったら応援しよう!