{"id":27,"date":"2025-08-28T16:55:21","date_gmt":"2025-08-28T07:55:21","guid":{"rendered":"https:\/\/www.decteng.com\/en\/?p=27"},"modified":"2025-08-28T16:55:22","modified_gmt":"2025-08-28T07:55:22","slug":"obs-youtube-hls-push-stream-capture-to-debug-pushcap","status":"publish","type":"post","link":"https:\/\/www.decteng.com\/en\/obs-youtube-hls-push-stream-capture-to-debug-pushcap\/","title":{"rendered":"OBS YouTube HLS PUSH Output Stream Capture and Save as Files"},"content":{"rendered":"\n<sectoin>\n<h2>PushCap<\/h2>\n<p>One of the challenges the writer faced while developing the <a href=\"https:\/\/www.decteng.com\/en\/how-to-hls-push-to-youtube-live-from-wowza\/\" title=\"How to PUSH HLS stream to YouTube from Wowza Streaming Engine\" hreflang=\"en\">YouTube HLS PUSH module for Wowza Streaming Engine<\/a> was validating the module&#8217;s output.<\/p>\n<p>The Wowza Streaming Engine&#8217;s logs showed that the module was working as expected and that network traffic was active. However, YouTube could not recognize the stream input. This led the writer to realize the need to compare the module&#8217;s output data with the data from <a href=\"https:\/\/obsproject.com\/\" target=\"_blank\" rel=\"noopener\">OBS (Open Broadcast Software)<\/a>.<\/p>\n<p>To solve this propose, the writer built a HLS data debug capture server with Python to save and check the incoming HLS PUSH data. And, named it <dfn>PushCap<\/dfn>, inspired by the memorable song shown below.<\/p>\n<center><iframe loading=\"lazy\" width=\"560\" height=\"315\" src=\"https:\/\/www.youtube.com\/embed\/glKyiUq_wWM?si=VS-z56yX14lS1-ep\" title=\"Jack Black - Hello Pusycat\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe><\/center>\n<\/section>\n\n<section>\n<h2>Running the Server<\/h2>\n<h3>Preparing the Environment<\/h3>\n<ol>\n<li><strong>Install Python<\/strong><p><code>PushCap<\/code> is a program based on Python. Before proceeding, ensure Python is installed on the PC that will act as the server. If needed, consult a <a href=\"https:\/\/realpython.com\/installing-python\/\" title=\"How to Install Python on Your System\" target=\"_blank\" hreflang=\"en\" rel=\"noopener\">guide<\/a> on how to install Python.<\/p><\/li>\n<li><strong>Install required packages<\/strong><p>Install <code>flask<\/code>package and <code>tornado<\/code> package to execute the script using the following command.<\/p><code class=\"line\">py -m pip install flask<\/code><code class=\"line\">py -m pip install tornado<\/code><\/li>\n<li><strong>Create an HTTPS Certificate<\/strong><p>Modern HLS protocols use HTTPS, which requires a certificate. Readers should generate one for this purpose. For detailed instructions, refer to the guide on <a href=\"https:\/\/www.decteng.com\/en\/wowza-https-certificate-for-hls-push-debug-python\" hreflang=\"en\" title=\"Apply a Self-Signed HTTPS Certificate to Wowza Streaming Engine for HLS PUSH\">Applying a Private HTTPS Certificate to Wowza Streaming Engine<\/a>.<\/p><\/li>\n<li><strong>Create the Program File<\/strong><p>Create a new directory. Then, copy the code provided below and save it in a file named <code>PushCap.py<\/code>.<\/p><\/li>\n<\/ol>\n<pre><code>import os\nimport ssl\nimport threading\nimport asyncio\nfrom flask import Flask, request, send_from_directory\nfrom tornado.wsgi import WSGIContainer\nfrom tornado.httpserver import HTTPServer\nfrom tornado.ioloop import IOLoop\n\napp = Flask(__name__)\nUPLOAD_DIR = os.path.join(os.path.dirname(__file__), 'uploads')\nplaylist_counters = {}\ncounter_lock = threading.Lock()\n\n@app.route('\/http_upload_hls', methods=['PUT'])\ndef hls_ingest():\n    try:\n        stream_key = request.args.get('cid')\n        file_name = request.args.get('file')\n\n        if not stream_key or not file_name:\n            return 'No file name or stream ID.', 400\n\n        stream_dir = os.path.join(UPLOAD_DIR, stream_key)\n        os.makedirs(stream_dir, exist_ok=True)\n        file_path = os.path.join(stream_dir, file_name)\n\n        request_body = request.environ['wsgi.input'].read()\n        \n        with open(file_path, 'wb') as f:\n            f.write(request_body)\n        \n        file_size = len(request_body)\n        if file_size == 0:\n            print(f'Warning: 0-byte file: {stream_key}\/{file_name}')\n        else:\n            print(f'\uc800\uc7a5: {stream_key}\/{file_name} ({file_size} bytes)')\n\t\t \/\/ print(f'saved: {stream_key}\/{file_name} ({file_size} bytes)')\t\n\n        if file_name.endswith('.m3u8'):\n            with counter_lock:\n                playlist_counters.setdefault(stream_key, {})\n                current_counter = playlist_counters[stream_key].get(file_name, 0)\n                playlist_counters[stream_key][file_name] = current_counter + 1\n            \n            base_name, extension = os.path.splitext(file_name)\n            new_file_name = f'{base_name}.{current_counter}{extension}'\n            new_file_path = os.path.join(stream_dir, new_file_name)\n            \n            with open(new_file_path, 'wb') as f:\n                f.write(request_body)\n            \n            print(f'-> M3U8 \uc21c\ucc28 \uc800\uc7a5 : {stream_key}\/{new_file_name}')\n\t\t\t\/\/print(f'-> M3U8 Saved with serial number : {stream_key}\/{new_file_name}')\n        \n        return 'OK', 200\n\n    except Exception as e:\n        print(f\"HLS ingest error: {e}\")\n        return 'Internal Server Error', 500\n\n@app.route('\/streams\/<string:stream_key>\/<string:file_name>', methods=['GET'])\ndef hls_playback(stream_key, file_name):\n    stream_dir = os.path.join(UPLOAD_DIR, stream_key)\n    return send_from_directory(stream_dir, file_name)\n\nif __name__ == '__main__':\n    async def main():\n        ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)\n        ssl_ctx.load_cert_chain(certfile='cert.pem', keyfile='key.pem')\n\n        http_server = HTTPServer(\n            WSGIContainer(app),\n            ssl_options=ssl_ctx\n        )\n        http_server.listen(443, address='0.0.0.0')\n        print(\"Starting PushCap HTTPS server on port 443...\")\n        print(\"\uc2a4\ud2b8\ub9bc\uc744 \uc3d8\uc138\uc694!\")\n\t\t\/\/print(\"Ready To receive!\")\n        await asyncio.Event().wait()\n    asyncio.run(main())<\/code><\/pre><\/li><\/ol>\n\n<h3>Execute the PushCap<\/h3>\n<p>In a console window, navigate to the directory containing <code>PushCap.py<\/code> and run it. If a message saying <samp>\uc2a4\ud2b8\ub9bc\uc744 \uc3d8\uc138\uc694!<\/samp>(Ready to receive!) is displayed, it means the program is running and ready to receive the HLS stream.<\/p>\n<pre><code>Microsoft Windows [Version 10.0.20348.4052]\n(c) Microsoft Corporation. All rights reserved.\nD:\\>cd HLSWEB\nD:\\HLSWEB><kbd>py PushCap.py<\/kbd>\nStarting PushCap HTTPS server on port 443...\n\uc2a4\ud2b8\ub9bc\uc744 \uc3d8\uc138\uc694!<\/code><\/pre>\n<\/section>\n\n<section>\n<h2>Capturing OBS&#8217;s YouTube HLS Output<\/h2>\n<h3>Modifying the hosts<\/h3>\n<p>OBS uses a hardcoded address for the default YouTube HLS ingest server. As a result, it always sends data directly to YouTube. To capture this traffic, readers must redirect this address to point to the server where <code>PushCap<\/code> is running.<\/p>\n<p>YouTube uses <code>a.upload.youtube.com<\/code> and <code>b.upload.youtube.com<\/code> as its HLS data upload addresses. When OBS tries to access them, the operating system looks up the corresponding IP address. By modifying the <code>hosts<\/code> file, which the OS checks first in this process, readers can redirect the connection away from the real YouTube servers.<\/p>\n<p>The <code>hosts<\/code> file is a plain text file with no extension, and it uses the same name on both Windows and Linux. The default paths for this file are as follows:\n<dl><dt>Windows<\/dt><dd><code>C:\\Windows\\System32\\drivers\\etc\\hosts<\/code><\/dd>\n<dt>Linux<\/dt><dd><code>\/etc\/hosts<\/code><\/dd><\/dl>\nUsing a text editor like <code>Notepad<\/code> or <code>nano<\/code>, readers should change the YouTube upload addresses to point to the IP address of the host running <code>PushCap<\/code>. The following is an example of a modified <code>hosts<\/code> file on <code>Windows<\/code>.<\/p>\n<pre><code># Copyright (c) 1993-2009 Microsoft Corp.\n#\n# This is a sample HOSTS file used by Microsoft TCP\/IP for Windows.\n#\n# This file contains the mappings of IP addresses to host names. Each\n# entry should be kept on an individual line. The IP address should\n# be placed in the first column followed by the corresponding host name.\n# The IP address and the host name should be separated by at least one\n# space.\n#\n# Additionally, comments (such as these) may be inserted on individual\n# lines or following the machine name denoted by a '#' symbol.\n#\n# For example:\n#\n#      102.54.94.97     rhino.acme.com          # source server\n#       38.25.63.10     x.acme.com              # x client host\n<kbd>127.0.0.1\ta.upload.youtube.com<\/kbd>\n<kbd># 127.0.0.1\tb.upload.youtube.com<\/kbd>\n# localhost name resolution is handled within DNS itself.\n#\t127.0.0.1       localhost\n#\t::1             localhost<\/code><\/pre>\n<p>In the example above, the main upload address is pointed to the local host where <code>PushCap<\/code> is running. This implies that <code>OBS<\/code> and <code>PushCap<\/code> are running on the one same server. The backup address line is prefixed with a <code>#<\/code>, which marks it as a comment, leaving it unchanged. The <code>hosts<\/code> file on Linux can be modified in the exact same way.<\/p>\n\n<h3>Capture the OBS HLS PUSH output stream<\/h3>\n<p><code>PushCap<\/code> is now ready to receive the stream. When readers start a YouTube HLS stream from <code>OBS<\/code>, <code>PushCap<\/code> will intercept and receive the stream data.<\/p>\n<ol><li><strong>Select HLS PUSH<\/strong><p>In the <code>OBS<\/code> settings window, navigate to the <samp>Stream<\/samp>. Then, select <code>YouTube-HLS<\/code> from the service list.<\/p><figure class=\"wp-block-image aligncenter size-800x500\"><a href=\"https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/OBS-YouTube-HLS-Destination.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"381\" src=\"https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/OBS-YouTube-HLS-Destination-800x381.webp\" alt=\"Select YouTube HLS on OBS\" class=\"wp-image-1163\" srcset=\"https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/OBS-YouTube-HLS-Destination-800x381.webp 800w, https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/OBS-YouTube-HLS-Destination-300x143.webp 300w, https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/OBS-YouTube-HLS-Destination-768x365.webp 768w, https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/OBS-YouTube-HLS-Destination.webp 971w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><\/a><\/figure><\/li>\n<li><strong>Set the Stream Key<\/strong><p>Select the <code>Use Stream Key<\/code> option. In the <samp>Stream Key<\/samp> field, enter any random value. The specific format or length does not matter. The key shown in the example screenshot is a random value created by the writer.<\/p><figure class=\"wp-block-image aligncenter size-800x500\"><a href=\"https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/OBS-YouTube-HLS-Destination-Set-Key.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"216\" src=\"https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/OBS-YouTube-HLS-Destination-Set-Key-800x216.webp\" alt=\"Input YouTube HLS stream key on OBS\" class=\"wp-image-1164\" srcset=\"https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/OBS-YouTube-HLS-Destination-Set-Key-800x216.webp 800w, https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/OBS-YouTube-HLS-Destination-Set-Key-300x81.webp 300w, https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/OBS-YouTube-HLS-Destination-Set-Key-768x207.webp 768w, https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/OBS-YouTube-HLS-Destination-Set-Key.webp 967w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><\/a><\/figure><\/li>\n<li><strong>Start Streaming<\/strong><p>Click the <code>Start Streaming<\/code> to start streaming. \uc744 \uc120\ud0dd\ud574, After starting the stream, readers can confirm that a connection is made to <code>PushCap<\/code> and that stream data is being uploaded.<\/p><figure class=\"wp-block-image aligncenter size-800x500\"><a href=\"https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/PushCap-Receiving-HLS-datas.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"457\" src=\"https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/PushCap-Receiving-HLS-datas-800x457.webp\" alt=\"PushCap HLS data ingest\" class=\"wp-image-1165\" srcset=\"https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/PushCap-Receiving-HLS-datas-800x457.webp 800w, https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/PushCap-Receiving-HLS-datas-300x171.webp 300w, https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/PushCap-Receiving-HLS-datas-768x439.webp 768w, https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/PushCap-Receiving-HLS-datas.webp 973w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><\/a><\/figure><\/li><\/ol>\n\n<h3>Checking the Captured HLS Data<\/h3>\n<p>After stopp the streaming in <code>OBS<\/code>, readers can check the directory containing the <code>PushCap<\/code> file. A new subdirectory named <code>upload<\/code> will have been created. Inside this directory, there will be another folder named after the stream key used, which contains the captured HLS data files from the <code>OBS<\/code>.<\/p>\n<figure class=\"wp-block-image aligncenter size-800x500\"><a href=\"https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/PushCap-upload-directory.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"582\" height=\"500\" src=\"https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/PushCap-upload-directory-582x500.webp\" alt=\"Captured HLS stream data by PushCap\" class=\"wp-image-1166\" srcset=\"https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/PushCap-upload-directory-582x500.webp 582w, https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/PushCap-upload-directory-300x258.webp 300w, https:\/\/www.decteng.com\/ko\/wp-content\/uploads\/sites\/4\/2025\/08\/PushCap-upload-directory.webp 624w\" sizes=\"auto, (max-width: 582px) 100vw, 582px\" \/><\/a><\/figure>\n\n<p>Files with the <code>.ts<\/code> extension are the video segments, saved exactly as uploaded by <code>OBS<\/code>. The <code>.m3u8<\/code> playlist files are saved with a sequential number each time when the <code>OBS<\/code> uploads a new version. The unnumbered <code>.m3u8<\/code> file is a copy of the last playlist uploaded by <code>OBS<\/code><\/p>\n\n<h3>Verifying the Data<\/h3>\n<p>Now, readers can verify that the captured data files were generated correctly. In the case of YouTube, readers can refer to the <a href=\"https:\/\/developers.google.com\/youtube\/v3\/live\/guides\/hls-ingestion\" title=\"Delivering Live YouTube Content via HLS\" hreflang=\"en\" target=\"_blank\" rel=\"noopener\">YouTube HLS Ingestion specifications<\/a> to verify that the captured data conforms to the required HLS format.<\/p>\n<\/section>\n\n<section>\n<h2>Conclusion<\/h2>\n<p>The best way to confirm that a program is working correctly is to verify that its output is as expected. The <code>PushCap<\/code> allows readers to directly inspect the HLS stream output from their streaming programs. The writer hopes this article has provided a helpful hint for readers who need to verify HLS stream data.<\/p><\/section>\n\n<section>\n<h2>FAQ<\/H2>\n<dl>\n<dt>What does HLS streaming data consist of?<\/dt><dd>HLS streaming data consists of <code>.ts<\/code> files, which contain the actual video data, and <code>.m3u8<\/code> files, which are playlists that define the playback order of the video files.<\/dd>\n<dt>What information is included in an .m3u8 file?<\/dt>\n<dd>It includes the m3u8 version, the duration of each video segment, playback sequence numbers, and the names of the video files for each sequence. Additionally, it can contain data for SCTE-35 digital ad markers and information for adaptive bitrate streaming, such as different video files for various bitrates.<\/dd>\n<dt>What is the chunk size for OBS?<\/dt>\n<dd>For YouTube HLS streaming, OBS creates 2-second chunks. YouTube&#8217;s recommended chunk size between 2~4 seconds.<\/dd>\n<dt>What is the difference between YouTube&#8217;s HLS ingest and a general HLS server?<\/dt><dd>YouTube&#8217;s HLS PUSH URL includes parameters like <code>cid<\/code> and a filename. In contrast, general HLS servers typically require files to be uploaded directly to some fixed path. Because of this difference, some streaming software that supports HLS PUSH may still be incompatible with YouTube if it cannot handle this specific URL format.<\/dd>\n<\/dl>\n<\/section>\n","protected":false},"excerpt":{"rendered":"<p>This guide shows how to capture OBS YouTube HLS stream output and save m3u8\/ts files. Sharing the code and usage of PushCap, a Python-based HLS stream receiving server for capturing and debugging HLS PUSH streaming software<\/p>\n","protected":false},"author":1,"featured_media":26,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4],"tags":[16,13,18,24,25,21],"class_list":{"0":"post-27","1":"post","2":"type-post","3":"status-publish","4":"format-standard","5":"has-post-thumbnail","7":"category-ott","8":"tag-development","9":"tag-hls","10":"tag-live-streaming","11":"tag-ott","12":"tag-python","13":"tag-youtube","14":"content-layout-excerpt-thumb"},"_links":{"self":[{"href":"https:\/\/www.decteng.com\/en\/wp-json\/wp\/v2\/posts\/27","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.decteng.com\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.decteng.com\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.decteng.com\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.decteng.com\/en\/wp-json\/wp\/v2\/comments?post=27"}],"version-history":[{"count":2,"href":"https:\/\/www.decteng.com\/en\/wp-json\/wp\/v2\/posts\/27\/revisions"}],"predecessor-version":[{"id":29,"href":"https:\/\/www.decteng.com\/en\/wp-json\/wp\/v2\/posts\/27\/revisions\/29"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.decteng.com\/en\/wp-json\/wp\/v2\/media\/26"}],"wp:attachment":[{"href":"https:\/\/www.decteng.com\/en\/wp-json\/wp\/v2\/media?parent=27"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.decteng.com\/en\/wp-json\/wp\/v2\/categories?post=27"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.decteng.com\/en\/wp-json\/wp\/v2\/tags?post=27"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}