深入理解nginx mp4流媒体模块[上]
深入理解nginx mp4流媒体模块[中]
以下对各个mp4的加载过程依次进行分析。
1. 加载ftyp atom
加载ftyp atom的逻辑由ngx_http_mp4_read_ftyp_atom函数来完成,其最主要的逻辑就是将文件中读取到的ftyp atom放到ngx_http_mp4_file_t上下文的名字为ftyp_atom的ngx_chain_t中,为后续mp4文件内容发送做好准备。其源码如下:
static ngx_int_t
ngx_http_mp4_read_ftyp_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
u_char *ftyp_atom;
size_t atom_size;
ngx_buf_t *atom;
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 ftyp atom");
/* ftyp atom限制不能超过1024字节 */
if (atom_data_size > 1024
|| ngx_mp4_atom_data(mp4) + (size_t) atom_data_size > mp4->buffer_end)
{
ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
"\"%s\" mp4 ftyp atom is too large:%uL",
mp4->file.name.data, atom_data_size);
return NGX_ERROR;
}
/* ftyp在一个mp4中只能有1个 */
if (mp4->ftyp_atom.buf) {
ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
"duplicate mp4 ftyp atom in \"%s\"", mp4->file.name.data);
return NGX_ERROR;
}
/* 分配一个ftyp atom的存储空间 */
atom_size = sizeof(ngx_mp4_atom_header_t) + (size_t) atom_data_size;
ftyp_atom = ngx_palloc(mp4->request->pool, atom_size);
if (ftyp_atom == NULL) {
return NGX_ERROR;
}
/* 设置ftyp atom的大小和名字字段 */
ngx_mp4_set_32value(ftyp_atom, atom_size);
ngx_mp4_set_atom_name(ftyp_atom, 'f', 't', 'y', 'p');
/*
* only moov atom content is guaranteed to be in mp4->buffer
* during sending response, so ftyp atom content should be copied
*/
ngx_memcpy(ftyp_atom + sizeof(ngx_mp4_atom_header_t),
ngx_mp4_atom_data(mp4), (size_t) atom_data_size);
/* 将刚才分配的ftyp的存储空间装入mp4->ftyp_atom_buf的ngx_but_t中 */
atom = &mp4->ftyp_atom_buf;
atom->temporary = 1;
atom->pos = ftyp_atom;
atom->last = ftyp_atom + atom_size;
/* 最后将mp4->ftyp_atom_buf装入mp4->ftyp_atom的ngx_chain_t中 */
mp4->ftyp_atom.buf = atom;
mp4->ftyp_size = atom_size;
mp4->content_length = atom_size;
/* 跳过ftyp atom准备进行后面的分析 */
ngx_mp4_atom_next(mp4, atom_data_size);
return NGX_OK;
}
2. 加载mdat atom
mp4文件本身没有规定mdat和moov这两个atom到底哪个在前哪个在后,为了提升网络流媒体视频打开速度,一般是建议将moov放在mdat atom前面的,但是有时候还是会碰到mdat atom是放在moov atom前面的情况,ngx_http_mp4_module对这两种情况都能够支持。源码如下:
static ngx_int_t
ngx_http_mp4_read_mdat_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
ngx_buf_t *data;
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 mdat atom");
/* 一个mp4中只能有一个mdat atom */
if (mp4->mdat_atom.buf) {
ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
"duplicate mp4 mdat atom in \"%s\"", mp4->file.name.data);
return NGX_ERROR;
}
/* 设置mp4->mdat_data_buf并将其装入mp4->mdat_data的ngx_chain_t链中
* 因为mdat往往都很大,nginx不需要将其都加载到内存中,而是采用ngx_buf_t支持的
* 文件类型的ngx_buf_t使用方法。
*/
data = &mp4->mdat_data_buf;
data->file = &mp4->file;
data->in_file = 1;
data->last_buf = (mp4->request == mp4->request->main) ? 1 : 0;
data->last_in_chain = 1;
data->file_last = mp4->offset + atom_data_size;
mp4->mdat_atom.buf = &mp4->mdat_atom_buf;
mp4->mdat_atom.next = &mp4->mdat_data;
mp4->mdat_data.buf = data;
if (mp4->trak.nelts) {
/* 这种情况是mdat在moov后面的理想情况,mdat atom后面就没有东西了,
直接将文件读指针调整到最末尾
*/
mp4->offset = mp4->end;
} else {
/* 这种情况是mdat在moove前面,mdat后面还需要继续加载moov atom
所以按照atom_data_size跳过mdat atom,继续进行扫描
*/
ngx_mp4_atom_next(mp4, atom_data_size);
}
return NGX_OK;
}
3. 加载moov atom
moov atom的读取由ngx_http_mp4_read_moov_atom函数来完成,其主要逻辑是递归调用ngx_http_mp4_read来加载各个子atom。源码如下:
/*
* Small excess buffer to process atoms after moov atom, mp4->buffer_start
* will be set to this buffer part after moov atom processing.
*/
#define NGX_HTTP_MP4_MOOV_BUFFER_EXCESS (4 * 1024)
static ngx_int_t
ngx_http_mp4_read_moov_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
ngx_int_t rc;
ngx_uint_t no_mdat;
ngx_buf_t *atom;
ngx_http_mp4_conf_t *conf;
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 moov atom");
/* no_mdat表示前面没有碰到过mdat atom */
no_mdat = (mp4->mdat_atom.buf == NULL);
/* 如果用户请求中指定了时间起始偏移为0,并且没有指定结束时间偏移,
那么就简化流程,返回整个文件,不需要进一步处理了
*/
if (no_mdat && mp4->start == 0 && mp4->length == 0) {
/*
* send original file if moov atom resides before
* mdat atom and client requests integral file
*/
return NGX_DECLINED;
}
/* 一个mp4文件中只能有一个moov atom */
if (mp4->moov_atom.buf) {
ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
"duplicate mp4 moov atom in \"%s\"", mp4->file.name.data);
return NGX_ERROR;
}
conf = ngx_http_get_module_loc_conf(mp4->request, ngx_http_mp4_module);
/* 判断当前的moov atom的大小是否超过了配置文件中设置的最大尺寸,
所以在提供服务前需要对库里面的视频文件的moov大小做一个预判。
*/
if (atom_data_size > mp4->buffer_size) {
if (atom_data_size > conf->max_buffer_size) {
ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
"\"%s\" mp4 moov atom is too large:%uL, "
"you may want to increase mp4_max_buffer_size",
mp4->file.name.data, atom_data_size);
return NGX_ERROR;
}
/* 如果只是超过了默认大小限制,那么重新分配缓冲区,
缓冲区的大小为moov atom的大小加上预读mdat的
NGX_HTTP_MP4_MOOV_BUFFER_EXCESS字节大小 */
*/
ngx_pfree(mp4->request->pool, mp4->buffer);
mp4->buffer = NULL;
mp4->buffer_pos = NULL;
mp4->buffer_end = NULL;
mp4->buffer_size = (size_t) atom_data_size
+ NGX_HTTP_MP4_MOOV_BUFFER_EXCESS * no_mdat;
}
/* 确保将整个moov读取到内存缓冲区中 */
if (ngx_http_mp4_read(mp4, (size_t) atom_data_size) != NGX_OK) {
return NGX_ERROR;
}
/* 初始化设置mp4的trak数组,默认支持2个trak */
mp4->trak.elts = &mp4->traks;
mp4->trak.size = sizeof(ngx_http_mp4_trak_t);
mp4->trak.nalloc = 2;
mp4->trak.pool = mp4->request->pool;
/* 将mp4->moov_atom_buf装载到mp4->moov_atom ngx_chain_t链中 */
atom = &mp4->moov_atom_buf;
atom->temporary = 1;
atom->pos = mp4->moov_atom_header;
atom->last = mp4->moov_atom_header + 8;
mp4->moov_atom.buf = &mp4->moov_atom_buf;
/* 递归调用ngx_http_mp4_read_atom,解析moov的子atom */
rc = ngx_http_mp4_read_atom(mp4, ngx_http_mp4_moov_atoms, atom_data_size);
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 moov atom done");
if (no_mdat) {
/* 对于mdat在moov后面的情况
重置缓冲区的指针,并且允许最大预读NGX_HTTP_MP4_MOOV_BUFFER_EXCESS
的mdat数据
*/
mp4->buffer_start = mp4->buffer_pos;
mp4->buffer_size = NGX_HTTP_MP4_MOOV_BUFFER_EXCESS;
if (mp4->buffer_start + mp4->buffer_size > mp4->buffer_end) {
mp4->buffer = NULL;
mp4->buffer_pos = NULL;
mp4->buffer_end = NULL;
}
} else {
/* 对于mdat在前面的情况,读取到moov后,就可以结束了 */
mp4->offset = mp4->end;
}
return rc;
}
4. 加载mvhd atom
mvhd作为moov的子atom,其加载逻辑由ngx_http_mp4_read_mvhd_atom函数来提供,它会在mvhd atom中解析mp4文件的timescale和duration两个字段,其中timescale字段的含义是每个mp4中的时间戳的一个单位对应的是多少分之一秒时间。这里需要判断请求的视频起始时间是否已经超过了mp4的总时长,如果超过了那么肯定是不合法的请求,nginx就会返回错误。另外,因为请求的视频内容