HLS配信からのM3U8プレイリスト抽出とTSセグメント結合の実装

動画配信プラットフォームからストリーミングデータを取得する場合、多くのサービスはHLS(HTTP Live Streaming)規格を採用しており、.m3u8プレイリストと.tsセグメントファイルの組み合わせで構成されています。本記事では、対象APIへのリクエスト構造を解析し、プレイリストの抽出からセグメントの結合までをPythonで実装する手順を解説します。

1. メタデータAPIへのリクエストとプレイリストURLの抽出

まず、動画の再生情報を含むJSONデータを取得するために、プロキシエンドポイントに対してPOSTリクエストを送信します。リクエストペイロードには、広告パラメータ、プラットフォーム情報、認証トークン、および暗号化されたキーなどが含まれます。適切なRefererCookieをヘッダーに設定しないと、サーバー側でアクセスが拒否される点に注意が必要です。

レスポンスとして返されるvinfoフィールドには、動画の再生URLが埋め込まれています。正規表現を用いて.m3u8形式のプレイリストURLを抽出します。

2. M3U8プレイリストの解析とTSセグメントの取得

取得したM3U8ファイルは、メタデータタグ(#EXTM3U#EXTINFなど)と実際のメディアセグメントのパスが混在したテキストファイルです。ダウンロード処理を簡素化するため、行単位で読み込み、#で始まるコメント行を除外してTSファイルのリストのみをフィルタリングします。

3. セグメントのダウンロードとバイナリ結合

HLS配信では動画が複数の小さなTSファイルに分割されています。これらを単一のMP4ファイルとして再生可能にするには、取得した順序通りにバイナリデータとして結合する必要があります。Pythonのファイルオープンモードで'ab'(追記バイナリモード)を指定することで、既存のファイル末尾に新しいセグメントデータを順次書き込むことができます。

以下の実装例では、リクエスト処理、URL抽出、プレイリスト解析、ファイル結合を関数に分割し、可読性と保守性を高めています。

import requests
import re
from tqdm import tqdm
from pathlib import Path

TARGET_API = "https://vd6.l.qq.com/proxyhttp"
OUTPUT_VIDEO = "downloaded_video.mp4"
SEGMENT_BASE = "https://ltsgdty.gtimg.com/B_MWKVMPpbmmy3lFOlyMFQVb6rtLRlH2V5NtArFWKYahE0djTdSn9bumr7eGey1VLo/svp_50112/sO2V1dWnlkKToKWRfOzI3cR3UhiZ17Z-Skp35KHHp4MrMsBZZetWMgF7OU_vY0Bvjib80b12PzHdbwUwdHwTFffu-wyt7np394Z2MmODF6Z9cJdqcjkIwEIE-F36tNBvL7lvhKZ4GVX_0kcgYIk820iFamTdTC_HWF403MZCQhkJqDjSAzdUpGJu5ptbogcxJY2JN0qIJi4rWxoGaCcT0itnOnp0fdYmoya4rYNcteV_wx-M3xp_qg/"

def request_video_metadata():
    payload = {
        "adparam": "adType=preAd&vid=i0046sewh4r&sspKey=wswd",
        "buid": "vinfoad",
        "sspAdParam": "{\"ad_scene\":1,\"pre_ad_params\":{\"ad_scene\":1,\"user_type\":2,\"video\":{\"base\":{\"vid\":\"i0046sewh4r\",\"cid\":\"mzc00200xf3rir6\"},\"is_live\":false,\"type_id\":3,\"referer\":\"https://v.qq.com/channel/cartoon\",\"url\":\"https://v.qq.com/x/cover/mzc00200xf3rir6/i0046sewh4r.html\",\"flow_id\":\"12c49b0148a070b463c19b22b07a2034\",\"refresh_id\":\"7603483fb7735e21aa757c17a941620b_1708859388\",\"fmt\":\"hd\"},\"platform\":{\"guid\":\"5c0d295e0ff4d662\",\"channel_id\":0,\"site\":\"web\",\"platform\":\"in\",\"from\":0,\"device\":\"pc\",\"play_platform\":10201,\"pv_tag\":\"cn_bing_com|channel\",\"support_click_scan_integration\":true},\"player\":{\"version\":\"1.30.10\",\"plugin\":\"3.4.49\",\"switch\":1,\"play_type\":\"0\"},\"token\":{\"type\":1,\"vuid\":394241996,\"vuser_session\":\"yBkJUvSaBx9BoemqfNvI9g.M\",\"app_id\":\"101483052\",\"open_id\":\"67CCE33CD137331E1EBE8BEB3A673CA9\",\"access_token\":\"10C9D6EB7957BF26D29F1FC1E0C6C584\"}}}",
        "vinfoparam": "charge=0&otype=ojson&defnpayver=3&spau=1&spaudio=0&spwm=1&sphls=2&host=v.qq.com&refer=https%3A%2F%2Fv.qq.com%2Fx%2Fcover%2Fmzc00200xf3rir6%2Fi0046sewh4r.html&ehost=https%3A%2F%2Fv.qq.com%2Fx%2Fcover%2Fmzc00200xf3rir6%2Fi0046sewh4r.html&sphttps=1&encryptVer=9.2&cKey=EdO-OrXxl6W1Mc1Orq2-LnCjnpb8Ocr0cPTcH_2PzEul_f4vOWcoUmJPd8rTtrI9OVVA0wNXkhhz_4LVGCTZlxo--_XGuZWLjvmpaTXWr2DNOLGAi4iBp92IyPY9pJacm0SuoYMK5cclDyKh6cqchEKDlC2uGzpCCamWfdlZnHB5x6wVJHKsVyf3svRoLvLp4yt5irIvBazTFI6klX7qSYlxDPbyyf8e8SL3SEnu3RmvgibC5O696HeQxGSo4VOM35yfH6ml6Md27yDn8qJE56sunEyjWSbSstS_5B9-E8AnI4GF1djs5-uKo6GxqIcXJIUV87h2tMf3dK_nyIsyhKhOxYaKK-hZOuXwJ_9Azca2s1AGdWwZi7dSQjVD3QIqMKZsPLJBfqdonJgzpKI-zf7tPsma4I3itUH2mrJQ1t1eiMXOOgHDgPCj5Hqs5MBScLzt8CKeI50CFA5m8jmdx0UZ9fmN6AoQSIe8G9z4YoaLU_i1lZrcZ9k3VTDRFzAKGxinZ2F5WudDNC1ArujtJDazWU-cAwMDemsQ-A&clip=4&guid=5c0d295e0ff4d662&flowid=12c49b0148a070b463c19b22b07a2034&platform=10201&sdtfrom=v1010&appVer=1.30.10&unid=&auth_from=&auth_ext=&vid=i0046sewh4r&defn=&fhdswitch=0&dtype=3&spsrt=2&tm=1708859485&lang_code=0&logintoken=%7B%22access_token%22%3A%2210C9D6EB7957BF26D29F1FC1E0C6C584%22%2C%22appid%22%3A%22101483052%22%2C%22vusession%22%3A%22yBkJUvSaBx9BoemqfNvI9g.M%22%2C%22openid%22%3A%2267CCE33CD137331E1EBE8BEB3A673CA9%22%2C%22vuserid%22%3A%22394241996%22%2C%22video_guid%22%3A%225c0d295e0ff4d662%22%2C%22main_login%22%3A%22qq%22%7D&spvvpay=1&spadseg=3&spav1=15&hevclv=28&spsfrhdr=0&spvideo=0&spm3u8tag=67&spmasterm3u8=3&drm=40"
    }
    req_headers = {
        "Referer": "https://v.qq.com/",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        "Cookie": "RK=CDOsEWVy0h; ptcz=5f5910e759fd2884ffd2d52d1c28327f145b4c6a0ccca7e5c70c008ad77238bc; eas_sid=61l77025D7U361j4I7T540X3f0; pgv_pvid=6319095440; _qimei_uuid42=181140e1137100212f0fb5b9ad2749c71807166555; _qimei_fingerprint=5d6fc68e5584b64e7b5bba3d425a25c2; _qimei_q36=; _qimei_h38=e147f0c72f0fb5b9ad2749c702000006b18114; qq_domain_video_guid_verify=5c0d295e0ff4d662; o_minduid=8XsP05UE18-_djq9QKbhDmdsNPlsDWs7; pgv_info=ssid=s4700628002; vversion_name=8.2.95; video_omgid=5c0d295e0ff4d662; LPVLturn=898; LPLFturn=522; LPSJturn=300; LVINturn=440; LPHLSturn=603; LDERturn=442; LPPBturn=391; appuser=65C6CEEF9C26AFAB; ad_session_id=e5ri32oscp4dc; LZTturn=513; ufc=r64_1_1708859447"
    }
    res = requests.post(TARGET_API, json=payload, headers=req_headers)
    res.raise_for_status()
    return res.json().get("vinfo", "")

def extract_m3u8_link(raw_vinfo):
    urls = re.findall(r'url":"(https?://[^"]+\.m3u8[^"]*)"', raw_vinfo)
    if not urls:
        raise RuntimeError("M3U8プレイリストのURLが見つかりません")
    return urls[0]

def fetch_ts_list(m3u8_uri):
    res = requests.get(m3u8_uri)
    res.raise_for_status()
    clean_lines = [
        line.strip() for line in res.text.splitlines()
        if line.strip() and not line.startswith("#")
    ]
    return clean_lines

def merge_segments(ts_filenames, save_path):
    target_file = Path(save_path)
    for chunk_name in tqdm(ts_filenames, desc="動画セグメント結合中"):
        chunk_url = f"{SEGMENT_BASE}{chunk_name}"
        chunk_res = requests.get(chunk_url)
        chunk_res.raise_for_status()
        with target_file.open("ab") as fout:
            fout.write(chunk_res.content)

if __name__ == "__main__":
    vinfo_data = request_video_metadata()
    playlist_url = extract_m3u8_link(vinfo_data)
    print(f"プレイリストURL: {playlist_url}")

    ts_segments = fetch_ts_list(playlist_url)
    merge_segments(ts_segments, OUTPUT_VIDEO)
    print("処理完了")

実装上の注意点

  • APIエンドポイントの特定: 配信プラットフォームによっては、XHRリクエストの中に直接M3U8のURLが含まれていない場合があります。ネットワークタブで断片的なTSリクエストをキャッチし、そのクエリパラメータを基に上位のメタデータAPIを逆算する必要があるケースも少なくありません。
  • アクセス頻度の制御: 連続したセグメントリクエストを短時間で送信すると、サーバー側のレート制限やWAF(Web Application Firewall)によってIPアドレスが一時的にブロックされる可能性があります。実運用ではリクエスト間に適切なディレイを設けるか、セッション管理を適切に行う必要があります。
  • ファイル結合の仕組み: TS形式はMPEG-2トランスポートストリームに基づいており、単純なバイナリ連結でも再生互換性が保たれる設計になっています。これにより、外部のマルチメディアライブラリに依存せずにopen(path, 'ab')のみで結合処理が完結します。

タグ: Python requests HLS M3U8 TSセグメント

5月25日 07:13 投稿