HLS 출력 디버그용 간이 HLS PUSH 수신 서버로 OBS HLS PUSH 스트림 캡처하기

HLS data check

HLS PUSH 스트리밍 소프트웨어 출력 디버그를 위해 파이썬으로 구현한 HLS 수신 서버 PushCap의 코드와 사용 방법을 공유하고, hosts 파일을 수정해 실제 OBS의 유튜브 HLS PUSH 스트림 출력의 m3u8과 ts데이터를 파일로 캡춰하는 과정을 공유한다.

PushCap

본 필자가 와우자 스트리밍 엔진의 YouTube HLS PUSH 모듈을 개발하면서 가장 답답했던 것 중의 하나는, 과연 내가 만든 모듈이 제대로 동작 하는가 눈으로 직접 확인하기가 곤란했다는 것이다.

모듈을 동작시키는 와우자의 로그에는 분명 이 모듈이 정상적으로 동작하고 있었고 실제 데이터도 출력 되고 있었으나, 유튜브에서는 스트림을 인식 하지 못하는 상황이 계속 발생했던 것 이다. 이에 본 필자는, 모듈의 실제 출력 데이터들을 OBS(Open Broadcast Software)의 것과 비교해서 하나씩 비교할 필요를 느꼈다.

이를 위해 파이썬(Python)을 이용해, 수신한 HLS PUSH 데이터를 저장하고 확인하기 위한 디버그용 캡처 서버를 만들었고, 마침 기억난 아래의 노래에서 따와 PushCap이라고 이름을 붙였다.

서버 실행

실행 환경의 준비

  1. 파이썬 설치

    PushCap은 파이썬을 기반으로 작동되는 프로그램이다. 파이썬 설치 방법과 같은 글을 참고하여 서버로 사용할 PC에 파이썬을 설치한다.

  2. 추가 패키치 설치

    프로그램의 실행에 필요한 추가 패키지를 설치해 준다. flask패키지와 tornado패키지, 가 필요하며, 다음 명령을 통해 설치가 가능한다.

    py -m pip install flaskpy -m pip install tornado
  3. https 인증서 생성

    현대 HLS 프로토콜은 https를 이용해 이루어 진다. 이를 위해 인증서를 생성해 주어야 한다. 자세한 내용은 와우자 스트리밍 엔진에 https 사설 인증서를 적용하는 방법를 참고하기 바란다.

  4. 프로그램 파일 생성

    적당한 디렉토리를 생성하고, 아래의 코드 내용을 복사해 PushCap.py라는 파일로 저장해 준다.

    import os
    import ssl
    import threading
    import asyncio
    from flask import Flask, request, send_from_directory
    from tornado.wsgi import WSGIContainer
    from tornado.httpserver import HTTPServer
    from tornado.ioloop import IOLoop
    
    app = Flask(__name__)
    UPLOAD_DIR = os.path.join(os.path.dirname(__file__), 'uploads')
    playlist_counters = {}
    counter_lock = threading.Lock()
    
    @app.route('/http_upload_hls', methods=['PUT'])
    def hls_ingest():
        try:
            stream_key = request.args.get('cid')
            file_name = request.args.get('file')
    
            if not stream_key or not file_name:
                return '스트림 ID나 FILE 이름이 없음.', 400
    
            stream_dir = os.path.join(UPLOAD_DIR, stream_key)
            os.makedirs(stream_dir, exist_ok=True)
            file_path = os.path.join(stream_dir, file_name)
    
            request_body = request.environ['wsgi.input'].read()
            
            with open(file_path, 'wb') as f:
                f.write(request_body)
            
            file_size = len(request_body)
            if file_size == 0:
                print(f'Warning: 0-byte file: {stream_key}/{file_name}')
            else:
                print(f'저장: {stream_key}/{file_name} ({file_size} bytes)')
    
            if file_name.endswith('.m3u8'):
                with counter_lock:
                    playlist_counters.setdefault(stream_key, {})
                    current_counter = playlist_counters[stream_key].get(file_name, 0)
                    playlist_counters[stream_key][file_name] = current_counter + 1
                
                base_name, extension = os.path.splitext(file_name)
                new_file_name = f'{base_name}.{current_counter}{extension}'
                new_file_path = os.path.join(stream_dir, new_file_name)
                
                with open(new_file_path, 'wb') as f:
                    f.write(request_body)
                
                print(f'-> M3U8 순차 저장 : {stream_key}/{new_file_name}')
            
            return 'OK', 200
    
        except Exception as e:
            print(f"HLS 입수 오류: {e}")
            return 'Internal Server Error', 500
    
    @app.route('/streams//', methods=['GET'])
    def hls_playback(stream_key, file_name):
        stream_dir = os.path.join(UPLOAD_DIR, stream_key)
        return send_from_directory(stream_dir, file_name)
    
    if __name__ == '__main__':
        async def main():
            ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
            ssl_ctx.load_cert_chain(certfile='cert.pem', keyfile='key.pem')
    
            http_server = HTTPServer(
                WSGIContainer(app),
                ssl_options=ssl_ctx
            )
            http_server.listen(443, address='0.0.0.0')
            print("Starting PushCap HTTPS server on port 443...")
            print("스트림을 쏘세요!")
            await asyncio.Event().wait()
        asyncio.run(main())

PushCap 실행

콘솔 창에서 PushCap.py이 있는 디렉토리로 이동해 PushCap.py를 실행시켜 준다. 아래와 같이, 스트림을 쏘세요!라는 메시지가 출력되면 프로그램이 실행되어 HLS를 수신할 준비가 완료 되었다는 뜻이다.

Microsoft Windows [Version 10.0.20348.4052]
(c) Microsoft Corporation. All rights reserved.
D:\>cd HLSWEB
D:\HLSWEB>py PushCap.py
Starting PushCap HTTPS server on port 443...
스트림을 쏘세요!

OBS YouTube HLS 출력 캡처

hosts 파일 수정

OBS는 기본 YouTube HLS ingest 주소를 프로그램 내부에 고정된 값으로 가지고 있다. 때문에, 무조건 실제 유튜브 서비스에 HLS 데이터를 전송한다. 그렇기 때문에, OBS가 유튜브의 업로드 주소를 찾을 때, PushCap이 실행되는 서버의 주소를 바라볼 수 있도록 변경해 주어야 한다.

유튜브의 업로드 주소는 a.upload.youtube.comb.upload.youtube.com가 사용되는데, OBS가 이 주소에 접근하려고 하면, 운영체제가 해당 주소의 IP를 확인하고 접근하도록 해 준다. 이 과정에서, 운영체제가 제일 먼저 확인하는 hosts라는 파일을 수정해 주면, 실제 YouTube가 아닌 다른 곳으로 접근하게 변경이 가능하다.

hosts 파일은 별도의 확장자가 없는 텍스트 파일로, 윈도우와 리눅스 모두 같은 이름을 사용한다. 이 파일이 있는 기본 경로는 다음과 같다.

윈도우
C:\Windows\System32\drivers\etc\hosts
리눅스
/etc/hosts
메모장이나 nano같은 텍스트 편집기를 이용해 유튜브 업로드 주소가 바라볼 IP 주소를 PushCap이 실행중인 호스트의 IP 로 변경해 준다. 아래는 윈도우hosts 파일을 수정한 예 이다.

# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
#      102.54.94.97     rhino.acme.com          # source server
#       38.25.63.10     x.acme.com              # x client host
127.0.0.1	a.upload.youtube.com
# 127.0.0.1	b.upload.youtube.com
# localhost name resolution is handled within DNS itself.
#	127.0.0.1       localhost
#	::1             localhost

위의 예에서는, 메인 업로드 주소를 PushCap이 실행중인 PC로 설정 했다. 즉, OBSPushCap이 동일한 서버에서 동작할 것이란 의미다. 예비주소의 경우, 줄 앞에 #이 붙어있다. 이것은 주석을 의미하며, 예비 주소에 대해서는 변경하지 않겠다는 의미이다. 리눅스 시스템의 hosts역시 동일한 방식으로 수정해 주면 된다.

OBS HLS PUSH 출력 캡처

PushCap의 설정이 완료 되었다. 이제 OBS프로그램에서 YouTube HLS 스트리밍을 시작하면 PushCap이 스트림 데이터를 수신하게 된다.

  1. HLS PUSH 서비스 선택

    OBS프로그램의 설정 화면에서 방송(Stream)을 선택 한 다음, 타겟 서비스 목록에서 YouTube-HLS를 선택해 준다.

    OBS의 유튜브 HLS 선택화면
  2. 스트림 키 설정

    스트림키 사용(Use Stream Key)를 선택해 스트림 키 입력 모드로 진입한 뒤, Stream Key항목에 임의의 키 번호를 입력해 준다. 여기에서 입력하는 항목은 굳이 자리수를 맞출 필요는 없다. 예시화면의 내용은 본 필자가 임의로 생성한 내용이다.

    OBS의 유튜브 HLS 스트림키 설정 화면
  3. 방송 시작

    방송 시작(Start Streaming)을 선택해, 스트리밍을 시작하면, PushCap에 연결되어 스트림 데이터를 업로드 하는 것을 확인할 수 있다.

  4. PushCap HLS 데이터 입수

HLS 캡처 데이터 확인

OBS의 스트리밍을 종료한 후, PushCap파일이 있는 디렉토리를 확인해 보면 upload라는 하위 디렉토리가 생성된 것을 확인할 수 있다. 해당 디렉토리 안에는 위에서 사용한 키 이름의 디렉토리가 있고, 그 안에는 OBS가 전송한 HLS 데이터를 캡처한 파일이 들어있다.

PushCap으로 캡처받은 HLS 데이터

확장자가 .ts인 파일은 동영상 데이터로, OBS가 업로드한 파일이 그대로 저장된다. 플레이리스트인 .m3u8파일은 OBS가 업로드 할 때 마다 번호를 붙여 저장한다. 마지막으로 번호가 붙지 않은 .m3u8파일은 OBS각 전송한 제일 마지막 파일로, 마지막 번호가 붙어있는 파일과 동일한 파일이다.

데이터 검증

캡처받은 데이터 파일들이 정상적으로 생성 되었는지 확인하면 된다. 유튜브의 경우에는 유튜브 HLS 라이브 콘텐츠 명세서를 참고하여, 캡처된 데이터들이 HLS 명세에 맞는 규격인지 확인해 보면 되겠다.

마무리

어떤 프로그램이 잘 동작하는지 확인하는 가장 좋은 방법은, 프로그램의 출력이 의도한 대로 나오는 가를 확인하는 것 이다. PushCap을 통해 스트리밍 프로그램의 HLS 스트림 데이터 출력을 직접 확인할 수 있다. 혹, HLS 스트리밍 프로그램이 제대로 동작하지 않아, m3u8 데이터가 정상적으로 생성 되었는지, ts파일의 포맷과 형태가 정상인지 확인해야 하는 상황에 놓이신 독자 제위께, 본 필자의 글이 작은 힌트가 되어드리기를 소망하는 바 이다.

FAQ

HLS 스트리밍 데이터에는 어떤 것들이 있는가?
HLS 스트리밍 데이터는 실제 동영상 데이터가 들어있는 .ts 파일과 동영상 데이터 파일을 재생할 순서에 대한 정보를 가지고 있는 .m3u8 파일로 구성된다.
.m3u8 파일에 들어가는 정보는?
이 m3u8 파일의 버전, 동영상 파일의 길이, 동영상 파일의 재생할 순서 번호, 각 순서별로 재생할 동영상 파일의 이름등이 들어간다. 추가적으로 SCTE-35 디지털 큐톤 데이터, 다중 비트레이트 대응을 위한 비트레이트별 동영상 파일 이름 등의 데이터도 존재 한다.
OBS의 청크 데이터 길이는?
OBS YouTube 청크는 2초를 기준으로 생성된다. 유튜브에서는 2~4초의 길이를 권장하고 있다.
유튜브의 HLS 입수 주소와 일반적인 HLS 서버와의 차이는?
유튜브의 HLS PUSH 주소는 cid와 파일 이름 매개변수를 포함한다. 반면, 보통의 HLS 서버들은 이미 확정된 경로의 위치에 파일을 직접 업로드 하는 방식을 사용한다. 때문에 HLS PUSH를 지원하는 스트리밍 소프트웨어라고 할 지라도 출력 주소의 형태에 따라 적용이 불가능한 경우가 있을 수 있다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다