Scroll to top

오디오 스트리밍 개발기4 – Streaming Response 206

벌써 오디오 스트리밍 개발기 세번째 이야기입니다.

지난 포스팅에서 오디오 스트리밍에서 중요한 요소중 하나가 콘텐츠 보호라고 했습니다.
이는 창작자에게 가장 중요한 요소가 될 수 있겠죠.
그렇다면 서비스를 이용하는 소비자에게 가장 중요한 요소는 어떤 것이 있을까요?
당연히, 안정적이고 문제없는 콘텐츠 소비가 되겠죠.

첫번째 포스팅에서 다뤘던 HTML Audio 객체를 이용하면 웹에서 오디오 파일을 쉽게 재생할 수 있었습니다.
이 Audio 객체에 두번째 포스팅에서 다뤘던 Onetime Link를 사용하면 자연스럽게 일회용 링크를 활용하면서
오디오 파일을 재생할 수 있겠죠.

하지만, 오디오 스트리밍 서비스는 이렇게 간단하지 않습니다..
당연한 얘기겠지만 10초 스킵이나 Seek 등이 필요하겠죠. 음악을 듣다보면 듣고 싶은 구간으로 이동하거나
구간반복을 하는 경우도 있습니다. 즉, 단순히 재생만 하는 것이 아니라 직접 제어를 하고 싶죠.
이러한 제어는 당연히 HTML Audio 객체가 제공하고 있습니다.

그렇다면 스트리밍 서버에서는 어떤 것을 제공해야 할까요?
사용자는 아마 “10초부터 재생해줘”, “100초부터 재생해줘” 등과 같은 요청을 보내겠죠.
이 기능은 HTML Audio 객체가 처리해줍니다.
그렇다면 이 요청을 받는 스트리밍 서버는 당연히 10초 위치의 콘텐츠를 전달해주거나, 100초 위치의 콘텐츠를 전달해줘야겠죠.
만약 HTML Audio 객체가 10초, 100초 위치부터 재생하라고 요청을 보냈는데, 서버가 그 위치에 대한 콘텐츠를 전달해주지 않는다면
처음부터 실행되거나 엉뚱한 콘텐츠가 재생될 수 있습니다.

먼저 HTML Audio 객체가 Seek 를 수행하면 어떤 데이터를 전달하는지 살펴보겠습니다.

Accept: */*
Accept-Encoding: identity;q=1, *;q=0
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Connection: keep-alive
Range: bytes=12615680-
Sec-Fetch-Dest: audio
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: same-origin

웹 서버에 전달하는 헤더 중 일부를 가져왔습니다.
헤더 항목중에 Range 항목에 bytes로 xxxx-로 표시된 부분이 있습니다.
이처럼 HTML Audio 객체는 사용자가 seek을 수행하면 그 지점의 bytes 주소를 같이 전달합니다.
위치, 콘텐츠 길이에 따라서 시작바이트주소-종료바이트주소로 표시됩니다.
즉 위 예시는 12,615,680 바이트 부터 데이터를 전달해달라는 의미입니다.

그렇다면 서버에서는 어떻게 처리를 할까요?

start_bytes = 0
end_bytes = length - 1

# 1. 요청에 Range 정보가 있는 경우 bytes 범위를 설정
if http_range:
    ranges = http_range.replace('bytes=', '').split('-')
    if ranges and ranges[0] and ranges[0].isdigit():
        start_bytes = int(ranges[0])
    if ranges and ranges[1] and ranges[1].isdigit():
        end_bytes = int(ranges[1])

# 2. 파일의 일부를 읽기 위한 함수
def file_iterator(file, pos):
    # file의 pos 위치로 이동하여 데이터를 반환하는 코드

# 3. start_bytes 위치부터 시작하는 데이터로 206 Response 생성
response = StreamingHttpResponse(file_iterator(path, start_bytes), content_type="audio/mp3", status=206)
# 4. Response 헤더에 전달하는 콘텐츠의 범위 정보를 포함
response['Content-Length'] = end_bytes - start_bytes + 1
response['Content-Range'] = 'bytes %d-%d/%d' % (start_bytes, end_bytes, length)
response['Accept-Ranges'] = 'bytes'

1번에서 먼저 요청의 헤더를 분석하여 Range 정보가 있는지 확인합니다.
2번에서는 파일의 원하는 위치부터 데이터를 가져올 수 있는 함수를 만듭니다.
3번에서는 2번에서 만든 함수를 활용하여 파일의 일부를 가져오고, 응답코드 206과 콘텐츠 타입을 지정하여 응답 객체를 생성합니다.
4번에서는 생성된 응답 객체 헤더에 범위 정보를 추가합니다.

과정이 조금 복잡하지만, 간단히 설명하자면 콘텐츠의 범위를 요청받으면 그 범위에 맞는 콘텐츠를 전달하고
실제로 해당 범위만큼 포함하고 있다는 정보를 함께 전달합니다.
여기서 중요한 부분은 상태코드를 206(Partial Content)으로 지정하지 않는다면,
HTML Audio 객체가 전달받은 콘텐츠가 일부라는 것을 모르기 때문에 정상적으로 동작하지 않습니다.

만약 ! 콘텐츠 일부 반환 처리와 상태코드 206을 반환하지 않는다면,
HTML Audio 객체는 Seek를 할 때마다 모든 콘텐츠를 다운받고 원하는 위치의 콘텐츠까지 다운받을 때까지 기다려야 합니다.
따라서 만약 10분짜리 콘텐츠를 재생하고 9분 55초를 눌렀을 때 콘텐츠가 9분 55초 지점까지 다운로드 될 때까지 재생이 되지 않습니다.

꼭! 스트리밍 서비스에서는 206 상태코드를 설정해줘야 합니다.