구글애드센스


[FFMPEG] Support for Apple's HLS Streaming STUDY



* What is Apple's HLS Streaming?

흔히 HLS Streaming 이라고 하면 애플의 HLS를 의미한다고 해도 과언이 아니다. Adobe의 HLS도 있는데 뭐가 먼저고 어떤 방식인지는 잘 모른다. (HDS 라고 부르나보다) 어쨌거나 이 페이지에서는 애플의 HLS에 대한 내용을 다룬다.
HLS는 HTTP Live Streeaming 의 약자이다. 풀이 하자면 Live Streaming over HTTP 가 되겠다. HTTP 프로토콜에 라이브 스트리밍 데이터를 실어 보내주겠다는 얘기다. 장점은 뭐 새로 프로토콜 정의할 필요가 없고, 추가적인 프로토콜을 뚫지 않아도 되며 다운로드 방식이라 서버에 부담도 적고(?) 적응형 스트리밍이 가능등.. 이 있는데 나같은 경우 모바일(안드로이드 3.0 이상 아이폰은 당근 지원) 지원때문에 공부를 했었다.

* How HLS Works?

HLS 는 크게 세 파트로 나눠진다. 그림으로 표현하자면 아래처럼 될 것이다.


(주의 : 각 용어는 내 마음대로 적은거라서 정확한 용어가 아님.)

첫 번째는 Segmentor 가 되겠다. 그렇다. 콩글리쉬일 가능성이 농후하다.  두 번째는 Playlist Maker, 세번째는 HTTPD 이다.
HLS는 서버쪽에서 할 일이 그렇게 많지가 않다. 제일 중요한 부분(?)은 HTTPD가 되겠다. 다른 말로 웹서버라 불리는 그것. Client에서 파일을 요청하면 HTTPD는 그냥 주면 된다. 물론 HTTPD의 Mimetype에 등록이 되어야하겠지만, 하여간 말이다.
그래서 HTTPD를 구현해야하느냐? 당연히 아니다. 우리에겐 이미 Apache 라는 훌륭한 HTTPD가 있다. 심지어 패키지 이름도 httpd잖아?
그러면 우리가 할 일은 뭘까? 바로 Segmentor와 Playlist Maker를 구현하는 것이다. 아쉽게도 Apache가 이런 것까지 해주진 않는다. 
Segmentor는 미디어 파일을 쪼개주는 녀석이다. HLS의 장점중에 다운로드 방식이라 서버에 부하를 줄여준다는 얘길 했었다. 좀 더 첨언하자면 스트리밍 하려는 데이터를 스트림 단위로 뽑아서 쏴주는 기타 Streaming 방식과는 달리 mpeg2ts 파일을 만드는데 이것을 특정 크기로 (사용자의 선택에 따라) 쪼개서 저장해둔다. 그리고 그 목록을 파일로 관리하는데 그것이 playlist 파일이고 클라이언트들은 playlist 파일에 의존해서 자기가 재생해야할 것들을 다운로드 하고 지가 알아서 플레이할 수 있게 되는 것이다.

 1
2
3
4
5
6
7
8
9
10
11
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:3
#EXT-X-MEDIA-SEQUENCE:18
#EXTINF:2.997000,
file018.ts
#EXTINF:2.997000,
file019.ts
#EXTINF:2.331000,
file020.ts
#EXT-X-ENDLIST

Playlist 파일은 대략 위와 같은 식으로 되어있는데 전체 내용을 설명하긴 좀 그렇고 따로 공부하기 바란다. 키워드는 M3U8 이다.
간략하게 설명하면 Segmentor는 영상 데이터를 길이별로 (보통은 길이) 잘라서 저장을 하고 playlist에 보관을 하며, playlist 파일은 어떤 파일을 다운로드 받아야 하는지, 각각 파일의 정확한 길이는 어떻게 되는지 등등의 정보를 보관하고 있다.
결국 이 Segmentor를 구현하려면 마치 RTP Packetizing 하듯이 h264 NAL 데이터를 잘라서 저장하는 로직을 구현해야 한다. (mpeg2ts 의 스펙을 공부해야함. 이 소스가 있긴 한데 나중에 공개하든지 할란다.)
ffmpeg이 아주 감사하게도, Segmentor와 Playlist Maker의 기능을 해준다. 각설하고 아래 예제코드를 참고하자. rtsp 소스를 받아서 약 100초간 hls로 만들어 주는 코드이다.

  1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/*
* Example created by muzie (mailto:sjy1937@hotmail.com)
*
* */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavformat/avio.h>
#include <libavdevice/avdevice.h>
}

int
main (void)
{
avdevice_register_all();
av_register_all();
avcodec_register_all();
avformat_network_init();
// Initialize

int vidx = 0, aidx = 0; // Video, Audio Index
AVFormatContext* ctx = avformat_alloc_context();
AVDictionary *dicts = NULL;
AVFormatContext* oc = NULL;

avformat_alloc_output_context2(&oc, NULL, "hls", "playlist.m3u8"); // apple hls. If you just want to segment file use "segment"

int rc = av_dict_set(&dicts, "rtsp_transport", "tcp", 0); // default udp. Set tcp interleaved mode
if (rc < 0)
{
return EXIT_FAILURE;
}

/*
rc = av_dict_set(&dicts, "stimeout", "1 * 1000 * 1000", 0); // timeout option
if (rc < 0)
{
return -1;
}
*/

//open rtsp
if (avformat_open_input(&ctx, "rtsp://211.189.132.118/nbr-media/media.nmp?ch=T142",NULL, &dicts) != 0)
{
return EXIT_FAILURE;
}

// get context
if (avformat_find_stream_info(ctx, NULL) < 0)
{
return EXIT_FAILURE;
}

//search video stream , audio stream
for (int i = 0 ; i < ctx->nb_streams ; i++)
{
if (ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
vidx = i;
if (ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO)
aidx = i;
}

AVPacket packet;

//open output file
if (oc == NULL)
{
return EXIT_FAILURE;
}

AVStream* vstream = NULL;
AVStream* astream = NULL;

vstream = avformat_new_stream(oc, ctx->streams[vidx]->codec->codec);
astream = avformat_new_stream(oc, ctx->streams[aidx]->codec->codec);

avcodec_copy_context(vstream->codec, ctx->streams[vidx]->codec);
vstream->time_base = (AVRational){1, 90000};
vstream->sample_aspect_ratio = ctx->streams[vidx]->codec->sample_aspect_ratio;

avcodec_copy_context(astream->codec, ctx->streams[aidx]->codec);
astream->time_base = (AVRational){1, 16000};

int cnt = 0;
long long nGap = 0;
long long nGap2 = 1;

av_read_play(ctx);//play RTSP

int i = (1 << 4); // omit endlist
int j = (1 << 1); // delete segment.
// libavformat/hlsenc.c 's description shows that no longer available files will be deleted but it doesnt works as described.

av_opt_set(oc->priv_data, "hls_segment_filename", "file%03d.ts", 0);
av_opt_set_int(oc->priv_data, "hls_list_size", 3, 0);
av_opt_set_int(oc->priv_data, "hls_time", 3, 0);
av_opt_set_int(oc->priv_data, "hls_flags", i|j, 0);

avformat_write_header(oc, NULL);

while (true)
{
av_init_packet(&packet);
int nRecvPacket = av_read_frame(ctx, &packet);

if (packet.stream_index == vidx) // video frame
{
vstream->id = vidx;
packet.pts = av_rescale_q(nGap, (AVRational){1, 10000}, oc->streams[packet.stream_index]->time_base);
nGap += 333;
cnt++;

}
else if (packet.stream_index == aidx) // audio frame
{
astream->id = aidx;
packet.pts = av_rescale_q(nGap2, (AVRational){1, 10000}, oc->streams[packet.stream_index]->time_base);
nGap2 += 666;
}

packet.dts = packet.pts;// generally, dts is same as pts. it only differ when the stream has b-frame
av_write_frame(oc,&packet);
av_packet_unref(&packet);

if (cnt > 3000) //
break;
}

av_read_pause(ctx);
av_write_trailer(oc);
avformat_free_context(oc);
av_dict_free(&dicts);

return (EXIT_SUCCESS);
}


* System Installation

 서버 - 웹 서버는 아파치 데몬을 설치해준다. 설정파일을 변경하여 root directory 를 avformat_alloc_output_context2 와 av_opt_set(oc->priv_data, "hls_segment_filename", "file%03d.ts", 0) 에서 설정해준 ts, m3u8 덤프 경로로 만들어 준다. 이 후 아파치 서버를 실행한다.
클라이언트 - PC 웹에서는 뭔가 작업을 해줘야하니까 같은 망에 물릴 수 있는 안드로이드 폰이나 아이폰의 웹브라우저를 열고, http://[설치서버IP]/playlist.m3u8 하면 영상을 바로 볼 수 있다.

소스 다운로드 : 클릭



덧글

  • 초보개발자 2017/01/05 18:03 # 삭제 답글

    안녕하세요 HLS에 대해 공부하는 학생입니다.
    HLS가 ts파일과 m3u8파일로 구성되고, 클라이언트 요청에 따라 m3u8파일 통해 스트리밍해주는 걸로 알고 있는데요,
    HTTP위에서 파일을 전송하기만 하면 되는거면 왜 꼭 ts파일과 m3u8파일로 전송하나요?
    그냥 mp4나 avi로 segment만 하여 전송하면 되는거 아닌가요?
  • muzie 2017/01/06 09:18 #

    거기까진 생각해본 적이 없는데 아마도 ts 파일 자체가 방송스트리밍용으로 개발된 컨테이너이기 때문이지 않나 싶습니다. 굳이 하려면 mp4나 avi로 할 수도 있겠지만 컨테이너 헤더 특성상 파싱이 간편하고 시퀀셜하게 핸들링이 가능하죠. 게다가 패킷당 고정 길이를 갖고 있으니 가공 자체도 편하니까요. 실제로 방송 시스템에서 사용하는 스트리밍 포맷이 udp에 ts를 실어서 보내는 경우도 봤습니다 (이게 표준인지는 모르겠습니다만 회사 프로젝시 경험해보았습니다.)

    정확한 정보는 아닙니다 ^^; 찾아봐도 검색이 안되네요.
  • 초보개발자 2017/01/10 09:32 # 삭제

    감사합니다!
  • 질문자 2017/03/16 17:15 # 삭제 답글

    안녕하세요 글이 도움이 많이 되었습니다. 감사합니다.
    질문 하나 할 수 있을까요?

    혹시 hls formatcontext인 oc 변수를 수정해여 생성되는 ts의 gop를 변경할 수 있나요?

    oc에는 streams 안의 codec밖에 codexcontext가 없어, 해당 변수를
    생성되는 ts에는 항상 gop가 3이고 I -> B -> P 순으로 프레임이 구성 될 수 있도록
    gop값을 3으로 변경하고
    max_b_frame을 1로 변경해보았는데

    gop 값이 적용되지 않습니다.

    어떻게 해야 제대로 적용될 수 있을까요?

    ps.
    카메라로 부터 영상을 받는다고 할 때, 카메라가 현재 찍고 있는 영상과
    ts로 만든 파일 사이의 delay가 존재할 수 밖에 없지만, 해당 delay를 최소한으로 하기 위해서는
    어떻게 하면 좋을까요..?

    av_write_frame과 avformat_write_header에 packet을 미리 받아 놓는 부분이 있는데 해당 부분을 수정 하면 될까요?

    감사합니다.
  • muzie 2017/03/17 20:49 #

    제가 이해한게 맞다면 format context 만 변경하셔서 GOP 크기와 B 사이즈를 조절하려고 하시는거 같은데 맞나요?
    이 소스는 RTSP 카메라로부터 영상을 수신받고 그것의 컨테이너만 TS로 설정하여 저장하는 것입니다. 원하시는대로 GOP Size를 변경하고 싶으시다면 카메라로부터 받은 NAL 데이터를 Decode 후 RAW를 획득 -> Encoder 설정 (GOP, B-Frame, Profile 등..)->Encode 해서 얻은 데이터를 ts로 저장하셔야 합니다.
  • 질문자 2017/03/20 09:31 # 삭제

    아~ encoding을 다시 해줘야 하는군요
    감사합니다!!
  • 찰리 2017/08/07 19:49 # 삭제 답글

    안녕하세요.
    FFMPEG로 플레이어를 제작하고 있는데 avformat_open_input으로 네트워크상의 ts 주소를 입력하여 사용하고있습니다.

    예를들면 avformat_open_input(&ctx,"http://192.168.0.2/test.ts");, 이런식으로 사용하고있는데,

    이때 실제 다운로드 받은 파일의 크기를 구하고 싶습니다. 어떤식으로 구해야할까요?

    조언 부탁드립니다.
  • muzie 2017/08/07 22:10 #

    통짜 ts 파일을 다운로드 받으시는건가요? 그런 것이라면 av_read_frame 할때 사이즈 구할 수 있으니 더하면 되겠죠?
    아니면 file close 하신 다음에 size를 읽으면 될 것이구요.

    실시간으로 확인해야 한다면 전자를, 아니면 후자를 택하시면 될 것 같습니다. ffmpeg api에 해당 기능이 있는지는 잘 모르겠네요.
댓글 입력 영역