/*
 * Copyright (C) Roman Arutyunyan
 */


#include <ngx_config.h>
#include <ngx_core.h>
#include "ngx_rtmp_play_module.h"
#include "ngx_rtmp_codec_module.h"
#include "ngx_rtmp_streams.h"


static ngx_int_t ngx_rtmp_mp4_postconfiguration(ngx_conf_t *cf);
static ngx_int_t ngx_rtmp_mp4_init(ngx_rtmp_session_t *s,  ngx_file_t *f,
       ngx_int_t aindex, ngx_int_t vindex);
static ngx_int_t ngx_rtmp_mp4_done(ngx_rtmp_session_t *s,  ngx_file_t *f);
static ngx_int_t ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_file_t *f);
static ngx_int_t ngx_rtmp_mp4_seek(ngx_rtmp_session_t *s,  ngx_file_t *f,
                                   ngx_uint_t offset);
static ngx_int_t ngx_rtmp_mp4_stop(ngx_rtmp_session_t *s,  ngx_file_t *f);
static ngx_int_t ngx_rtmp_mp4_send(ngx_rtmp_session_t *s,  ngx_file_t *f,
                                   ngx_uint_t *ts);
static ngx_int_t ngx_rtmp_mp4_reset(ngx_rtmp_session_t *s);


#define NGX_RTMP_MP4_MAX_FRAMES         8


#pragma pack(push,4)


/* disable zero-sized array warning by msvc */

#if (NGX_WIN32)
#pragma warning(push)
#pragma warning(disable:4200)
#endif


typedef struct {
    uint32_t                            first_chunk;
    uint32_t                            samples_per_chunk;
    uint32_t                            sample_descrption_index;
} ngx_rtmp_mp4_chunk_entry_t;


typedef struct {
    uint32_t                            version_flags;
    uint32_t                            entry_count;
    ngx_rtmp_mp4_chunk_entry_t          entries[0];
} ngx_rtmp_mp4_chunks_t;


typedef struct {
    uint32_t                            sample_count;
    uint32_t                            sample_delta;
} ngx_rtmp_mp4_time_entry_t;


typedef struct {
    uint32_t                            version_flags;
    uint32_t                            entry_count;
    ngx_rtmp_mp4_time_entry_t           entries[0];
} ngx_rtmp_mp4_times_t;


typedef struct {
    uint32_t                            sample_count;
    uint32_t                            sample_offset;
} ngx_rtmp_mp4_delay_entry_t;


typedef struct {
    uint32_t                            version_flags;
    uint32_t                            entry_count;
    ngx_rtmp_mp4_delay_entry_t          entries[0];
} ngx_rtmp_mp4_delays_t;


typedef struct {
    uint32_t                            version_flags;
    uint32_t                            entry_count;
    uint32_t                            entries[0];
} ngx_rtmp_mp4_keys_t;


typedef struct {
    uint32_t                            version_flags;
    uint32_t                            sample_size;
    uint32_t                            sample_count;
    uint32_t                            entries[0];
} ngx_rtmp_mp4_sizes_t;


typedef struct {
    uint32_t                            version_flags;
    uint32_t                            field_size;
    uint32_t                            sample_count;
    uint32_t                            entries[0];
} ngx_rtmp_mp4_sizes2_t;


typedef struct {
    uint32_t                            version_flags;
    uint32_t                            entry_count;
    uint32_t                            entries[0];
} ngx_rtmp_mp4_offsets_t;


typedef struct {
    uint32_t                            version_flags;
    uint32_t                            entry_count;
    uint64_t                            entries[0];
} ngx_rtmp_mp4_offsets64_t;


#if (NGX_WIN32)
#pragma warning(pop)
#endif


#pragma pack(pop)


typedef struct {
    uint32_t                            timestamp;
    uint32_t                            last_timestamp;
    off_t                               offset;
    size_t                              size;
    ngx_int_t                           key;
    uint32_t                            delay;

    unsigned                            not_first:1;
    unsigned                            valid:1;

    ngx_uint_t                          pos;

    ngx_uint_t                          key_pos;

    ngx_uint_t                          chunk;
    ngx_uint_t                          chunk_pos;
    ngx_uint_t                          chunk_count;

    ngx_uint_t                          time_pos;
    ngx_uint_t                          time_count;

    ngx_uint_t                          delay_pos;
    ngx_uint_t                          delay_count;

    ngx_uint_t                          size_pos;
} ngx_rtmp_mp4_cursor_t;


typedef struct {
    ngx_uint_t                          id;

    ngx_int_t                           type;
    ngx_int_t                           codec;
    uint32_t                            csid;
    u_char                              fhdr;
    ngx_int_t                           time_scale;
    uint64_t                            duration;

    u_char                             *header;
    size_t                              header_size;
    unsigned                            header_sent:1;

    ngx_rtmp_mp4_times_t               *times;
    ngx_rtmp_mp4_delays_t              *delays;
    ngx_rtmp_mp4_keys_t                *keys;
    ngx_rtmp_mp4_chunks_t              *chunks;
    ngx_rtmp_mp4_sizes_t               *sizes;
    ngx_rtmp_mp4_sizes2_t              *sizes2;
    ngx_rtmp_mp4_offsets_t             *offsets;
    ngx_rtmp_mp4_offsets64_t           *offsets64;
    ngx_rtmp_mp4_cursor_t               cursor;
} ngx_rtmp_mp4_track_t;


typedef struct {
    void                               *mmaped;
    size_t                              mmaped_size;
    ngx_fd_t                            extra;

    unsigned                            meta_sent:1;

    ngx_rtmp_mp4_track_t                tracks[2];
    ngx_rtmp_mp4_track_t               *track;
    ngx_uint_t                          ntracks;

    ngx_uint_t                          width;
    ngx_uint_t                          height;
    ngx_uint_t                          nchannels;
    ngx_uint_t                          sample_size;
    ngx_uint_t                          sample_rate;

    ngx_int_t                           atracks, vtracks;
    ngx_int_t                           aindex, vindex;

    uint32_t                            start_timestamp, epoch;
} ngx_rtmp_mp4_ctx_t;


#define ngx_rtmp_mp4_make_tag(a, b, c, d)  \
    ((uint32_t)d << 24 | (uint32_t)c << 16 | (uint32_t)b << 8 | (uint32_t)a)


static ngx_inline uint32_t
ngx_rtmp_mp4_to_rtmp_timestamp(ngx_rtmp_mp4_track_t *t, uint64_t ts)
{
    return (uint32_t) (ts * 1000 / t->time_scale);
}


static ngx_inline uint32_t
ngx_rtmp_mp4_from_rtmp_timestamp(ngx_rtmp_mp4_track_t *t, uint32_t ts)
{
    return (uint64_t) ts * t->time_scale / 1000;
}


#define NGX_RTMP_MP4_BUFLEN_ADDON       1000


static u_char                           ngx_rtmp_mp4_buffer[1024*1024];


#if (NGX_WIN32)
static void *
ngx_rtmp_mp4_mmap(ngx_fd_t fd, size_t size, off_t offset, ngx_fd_t *extra)
{
    void           *data;

    *extra = CreateFileMapping(fd, NULL, PAGE_READONLY,
                               (DWORD) ((uint64_t) size >> 32),
                               (DWORD) (size & 0xffffffff),
                               NULL);
    if (*extra == NULL) {
        return NULL;
    }

    data = MapViewOfFile(*extra, FILE_MAP_READ,
                         (DWORD) ((uint64_t) offset >> 32),
                         (DWORD) (offset & 0xffffffff),
                         size);

    if (data == NULL) {
        CloseHandle(*extra);
    }

    /*
     * non-NULL result means map view handle is open
     * and should be closed later
     */

    return data;
}


static ngx_int_t
ngx_rtmp_mp4_munmap(void *data, size_t size, ngx_fd_t *extra)
{
    ngx_int_t  rc;

    rc = NGX_OK;

    if (UnmapViewOfFile(data) == 0) {
        rc = NGX_ERROR;
    }

    if (CloseHandle(*extra) == 0) {
        rc = NGX_ERROR;
    }

    return rc;
}

#else

static void *
ngx_rtmp_mp4_mmap(ngx_fd_t fd, size_t size, off_t offset, ngx_fd_t *extra)
{
    void  *data;

    data = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, offset);

    /* valid address is never NULL since there's no MAP_FIXED */

    return data == MAP_FAILED ? NULL : data;
}


static ngx_int_t
ngx_rtmp_mp4_munmap(void *data, size_t size, ngx_fd_t *extra)
{
    return munmap(data, size);
}

#endif


static ngx_int_t ngx_rtmp_mp4_parse(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_trak(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_mdhd(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_hdlr(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_stsd(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_stsc(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_stts(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_ctts(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_stss(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_stsz(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_stz2(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_stco(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_co64(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_avc1(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_avcC(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_mp4a(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_mp4v(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_esds(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_mp3(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_nmos(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_spex(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);


typedef ngx_int_t (*ngx_rtmp_mp4_box_pt)(ngx_rtmp_session_t *s, u_char *pos,
                                         u_char *last);

typedef struct {
    uint32_t                            tag;
    ngx_rtmp_mp4_box_pt                 handler;
} ngx_rtmp_mp4_box_t;


static ngx_rtmp_mp4_box_t                       ngx_rtmp_mp4_boxes[] = {
    { ngx_rtmp_mp4_make_tag('t','r','a','k'),   ngx_rtmp_mp4_parse_trak   },
    { ngx_rtmp_mp4_make_tag('m','d','i','a'),   ngx_rtmp_mp4_parse        },
    { ngx_rtmp_mp4_make_tag('m','d','h','d'),   ngx_rtmp_mp4_parse_mdhd   },
    { ngx_rtmp_mp4_make_tag('h','d','l','r'),   ngx_rtmp_mp4_parse_hdlr   },
    { ngx_rtmp_mp4_make_tag('m','i','n','f'),   ngx_rtmp_mp4_parse        },
    { ngx_rtmp_mp4_make_tag('s','t','b','l'),   ngx_rtmp_mp4_parse        },
    { ngx_rtmp_mp4_make_tag('s','t','s','d'),   ngx_rtmp_mp4_parse_stsd   },
    { ngx_rtmp_mp4_make_tag('s','t','s','c'),   ngx_rtmp_mp4_parse_stsc   },
    { ngx_rtmp_mp4_make_tag('s','t','t','s'),   ngx_rtmp_mp4_parse_stts   },
    { ngx_rtmp_mp4_make_tag('c','t','t','s'),   ngx_rtmp_mp4_parse_ctts   },
    { ngx_rtmp_mp4_make_tag('s','t','s','s'),   ngx_rtmp_mp4_parse_stss   },
    { ngx_rtmp_mp4_make_tag('s','t','s','z'),   ngx_rtmp_mp4_parse_stsz   },
    { ngx_rtmp_mp4_make_tag('s','t','z','2'),   ngx_rtmp_mp4_parse_stz2   },
    { ngx_rtmp_mp4_make_tag('s','t','c','o'),   ngx_rtmp_mp4_parse_stco   },
    { ngx_rtmp_mp4_make_tag('c','o','6','4'),   ngx_rtmp_mp4_parse_co64   },
    { ngx_rtmp_mp4_make_tag('a','v','c','1'),   ngx_rtmp_mp4_parse_avc1   },
    { ngx_rtmp_mp4_make_tag('a','v','c','C'),   ngx_rtmp_mp4_parse_avcC   },
    { ngx_rtmp_mp4_make_tag('m','p','4','a'),   ngx_rtmp_mp4_parse_mp4a   },
    { ngx_rtmp_mp4_make_tag('m','p','4','v'),   ngx_rtmp_mp4_parse_mp4v   },
    { ngx_rtmp_mp4_make_tag('e','s','d','s'),   ngx_rtmp_mp4_parse_esds   },
    { ngx_rtmp_mp4_make_tag('.','m','p','3'),   ngx_rtmp_mp4_parse_mp3    },
    { ngx_rtmp_mp4_make_tag('n','m','o','s'),   ngx_rtmp_mp4_parse_nmos   },
    { ngx_rtmp_mp4_make_tag('s','p','e','x'),   ngx_rtmp_mp4_parse_spex   },
    { ngx_rtmp_mp4_make_tag('w','a','v','e'),   ngx_rtmp_mp4_parse        }
};


static ngx_int_t ngx_rtmp_mp4_parse_descr(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_es(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_dc(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);
static ngx_int_t ngx_rtmp_mp4_parse_ds(ngx_rtmp_session_t *s, u_char *pos,
       u_char *last);


typedef ngx_int_t (*ngx_rtmp_mp4_descriptor_pt)(ngx_rtmp_session_t *s,
                                                u_char *pos, u_char *last);

typedef struct {
    uint8_t                             tag;
    ngx_rtmp_mp4_descriptor_pt          handler;
} ngx_rtmp_mp4_descriptor_t;


static ngx_rtmp_mp4_descriptor_t        ngx_rtmp_mp4_descriptors[] = {
    { 0x03,   ngx_rtmp_mp4_parse_es   },    /* MPEG ES Descriptor */
    { 0x04,   ngx_rtmp_mp4_parse_dc   },    /* MPEG DecoderConfig Descriptor */
    { 0x05,   ngx_rtmp_mp4_parse_ds   }     /* MPEG DecoderSpec Descriptor */
};


static ngx_rtmp_module_t  ngx_rtmp_mp4_module_ctx = {
    NULL,                                   /* preconfiguration */
    ngx_rtmp_mp4_postconfiguration,         /* postconfiguration */
    NULL,                                   /* create main configuration */
    NULL,                                   /* init main configuration */
    NULL,                                   /* create server configuration */
    NULL,                                   /* merge server configuration */
    NULL,                                   /* create app configuration */
    NULL                                    /* merge app configuration */
};


ngx_module_t  ngx_rtmp_mp4_module = {
    NGX_MODULE_V1,
    &ngx_rtmp_mp4_module_ctx,               /* module context */
    NULL,                                   /* module directives */
    NGX_RTMP_MODULE,                        /* module type */
    NULL,                                   /* init master */
    NULL,                                   /* init module */
    NULL,                                   /* init process */
    NULL,                                   /* init thread */
    NULL,                                   /* exit thread */
    NULL,                                   /* exit process */
    NULL,                                   /* exit master */
    NGX_MODULE_V1_PADDING
};


static ngx_int_t
ngx_rtmp_mp4_parse_trak(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    ngx_rtmp_mp4_ctx_t         *ctx;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    if (ctx->track) {
        return NGX_OK;
    }

    ctx->track = (ctx->ntracks == sizeof(ctx->tracks) / sizeof(ctx->tracks[0]))
                 ? NULL : &ctx->tracks[ctx->ntracks];

    if (ctx->track) {
        ngx_memzero(ctx->track, sizeof(*ctx->track));
        ctx->track->id = ctx->ntracks;

        ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: trying track %ui", ctx->ntracks);
    }

    if (ngx_rtmp_mp4_parse(s, pos, last) != NGX_OK) {
        return NGX_ERROR;
    }

    if (ctx->track && ctx->track->type &&
        (ctx->ntracks == 0 ||
         ctx->tracks[0].type != ctx->tracks[ctx->ntracks].type))
    {
        ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: adding track %ui", ctx->ntracks);

        if (ctx->track->type == NGX_RTMP_MSG_AUDIO) {
            if (ctx->atracks++ != ctx->aindex) {
                ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                               "mp4: skipping audio track %ui!=%ui",
                               ctx->atracks - 1, ctx->aindex);
                ctx->track = NULL;
                return NGX_OK;
            }

        } else {
            if (ctx->vtracks++ != ctx->vindex) {
                ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                               "mp4: skipping video track %i!=%i",
                               ctx->vtracks - 1, ctx->vindex);
                ctx->track = NULL;
                return NGX_OK;
            }
        }

        ++ctx->ntracks;

    } else {
        ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: ignoring track %ui", ctx->ntracks);
    }

    ctx->track = NULL;

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_parse_mdhd(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    ngx_rtmp_mp4_ctx_t         *ctx;
    ngx_rtmp_mp4_track_t       *t;
    uint8_t                     version;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    if (ctx->track == NULL) {
        return NGX_OK;
    }

    t = ctx->track;

    if (pos + 1 > last) {
        return NGX_ERROR;
    }

    version = *(uint8_t *) pos;

    switch (version) {
        case 0:
            if (pos + 20 > last) {
                return NGX_ERROR;
            }

            pos += 12;
            t->time_scale = ngx_rtmp_r32(*(uint32_t *) pos);
            pos += 4;
            t->duration = ngx_rtmp_r32(*(uint32_t *) pos);
            break;

        case 1:
            if (pos + 28 > last) {
                return NGX_ERROR;
            }

            pos += 20;
            t->time_scale = ngx_rtmp_r32(*(uint32_t *) pos);
            pos += 4;
            t->duration = ngx_rtmp_r64(*(uint64_t *) pos);
            break;

        default:
            return NGX_ERROR;
    }

    ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: duration time_scale=%ui duration=%uL",
                   t->time_scale, t->duration);

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_parse_hdlr(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    ngx_rtmp_mp4_ctx_t         *ctx;
    uint32_t                    type;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    if (ctx->track == NULL) {
        return NGX_OK;
    }

    if (pos + 12 > last) {
        return NGX_ERROR;
    }

    type = *(uint32_t *)(pos + 8);

    if (type == ngx_rtmp_mp4_make_tag('v','i','d','e')) {
        ctx->track->type = NGX_RTMP_MSG_VIDEO;
        ctx->track->csid = NGX_RTMP_CSID_VIDEO;

        ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: video track");

    } else if (type == ngx_rtmp_mp4_make_tag('s','o','u','n')) {
        ctx->track->type = NGX_RTMP_MSG_AUDIO;
        ctx->track->csid = NGX_RTMP_CSID_AUDIO;

        ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: audio track");
    } else {
        ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: unknown track");
    }

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_parse_video(ngx_rtmp_session_t *s, u_char *pos, u_char *last,
                         ngx_int_t codec)
{
    ngx_rtmp_mp4_ctx_t         *ctx;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    if (ctx->track == NULL) {
        return NGX_OK;
    }

    ctx->track->codec = codec;

    if (pos + 78 > last) {
        return NGX_ERROR;
    }

    pos += 24;

    ctx->width = ngx_rtmp_r16(*(uint16_t *) pos);

    pos += 2;

    ctx->height = ngx_rtmp_r16(*(uint16_t *) pos);

    pos += 52;

    ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: video settings codec=%i, width=%ui, height=%ui",
                   codec, ctx->width, ctx->height);

    if (ngx_rtmp_mp4_parse(s, pos, last) != NGX_OK) {
        return NGX_ERROR;
    }

    ctx->track->fhdr = (u_char) ctx->track->codec;

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_parse_audio(ngx_rtmp_session_t *s, u_char *pos, u_char *last,
                         ngx_int_t codec)
{
    ngx_rtmp_mp4_ctx_t         *ctx;
    u_char                     *p;
    ngx_uint_t                  version;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    if (ctx->track == NULL) {
        return NGX_OK;
    }

    ctx->track->codec = codec;

    if (pos + 28 > last) {
        return NGX_ERROR;
    }

    pos += 8;

    version = ngx_rtmp_r16(*(uint16_t *) pos);

    pos += 8;

    ctx->nchannels = ngx_rtmp_r16(*(uint16_t *) pos);

    pos += 2;

    ctx->sample_size = ngx_rtmp_r16(*(uint16_t *) pos);

    pos += 6;

    ctx->sample_rate = ngx_rtmp_r16(*(uint16_t *) pos);

    pos += 4;

    p = &ctx->track->fhdr;

    *p = 0;

    if (ctx->nchannels == 2) {
        *p |= 0x01;
    }

    if (ctx->sample_size == 16) {
        *p |= 0x02;
    }

    switch (ctx->sample_rate) {
        case 5512:
            break;

        case 11025:
            *p |= 0x04;
            break;

        case 22050:
            *p |= 0x08;
            break;

        default:  /*44100 etc */
            *p |= 0x0c;
            break;
    }

    ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: audio settings version=%ui, codec=%i, nchannels==%ui, "
                   "sample_size=%ui, sample_rate=%ui",
                   version, codec, ctx->nchannels, ctx->sample_size,
                   ctx->sample_rate);

    switch (version) {
        case 1:
            pos += 16;
            break;

        case 2:
            pos += 36;
    }

    if (pos > last) {
        return NGX_ERROR;
    }

    if (ngx_rtmp_mp4_parse(s, pos, last) != NGX_OK) {
        return NGX_ERROR;
    }

    *p |= (ctx->track->codec << 4);

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_parse_avc1(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    return ngx_rtmp_mp4_parse_video(s, pos, last, NGX_RTMP_VIDEO_H264);
}


static ngx_int_t
ngx_rtmp_mp4_parse_mp4v(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    return ngx_rtmp_mp4_parse_video(s, pos, last, NGX_RTMP_VIDEO_H264);
}


static ngx_int_t
ngx_rtmp_mp4_parse_avcC(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    ngx_rtmp_mp4_ctx_t         *ctx;

    if (pos == last) {
        return NGX_OK;
    }

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    if (ctx->track == NULL || ctx->track->codec != NGX_RTMP_VIDEO_H264) {
        return NGX_OK;
    }

    ctx->track->header = pos;
    ctx->track->header_size = (size_t) (last - pos);

    ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: video h264 header size=%uz",
                   ctx->track->header_size);

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_parse_mp4a(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    return ngx_rtmp_mp4_parse_audio(s, pos, last, NGX_RTMP_AUDIO_MP3);
}


static ngx_int_t
ngx_rtmp_mp4_parse_ds(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    ngx_rtmp_mp4_ctx_t     *ctx;
    ngx_rtmp_mp4_track_t   *t;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    t = ctx->track;

    if (t == NULL) {
        return NGX_OK;
    }

    t->header = pos;
    t->header_size = (size_t) (last - pos);

    ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: decoder header size=%uz", t->header_size);

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_parse_dc(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    uint8_t                 id;
    ngx_rtmp_mp4_ctx_t     *ctx;
    ngx_int_t              *pc;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    if (ctx->track == NULL) {
        return NGX_OK;
    }

    if (pos + 13 > last) {
        return NGX_ERROR;
    }

    id = * (uint8_t *) pos;
    pos += 13;
    pc = &ctx->track->codec;

    switch (id) {
        case 0x21:
            *pc = NGX_RTMP_VIDEO_H264;
            break;

        case 0x40:
        case 0x66:
        case 0x67:
        case 0x68:
            *pc = NGX_RTMP_AUDIO_AAC;
            break;

        case 0x69:
        case 0x6b:
            *pc = NGX_RTMP_AUDIO_MP3;
            break;
    }

    ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: decoder descriptor id=%i codec=%i",
                   (ngx_int_t) id, *pc);

    return ngx_rtmp_mp4_parse_descr(s, pos, last);
}


static ngx_int_t
ngx_rtmp_mp4_parse_es(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    uint16_t    id;
    uint8_t     flags;

    if (pos + 3 > last) {
        return NGX_ERROR;
    }

    id = ngx_rtmp_r16(*(uint16_t *) pos);
    pos += 2;

    flags = *(uint8_t *) pos;
    ++pos;

    if (flags & 0x80) { /* streamDependenceFlag */
        pos += 2;
    }

    if (flags & 0x40) { /* URL_FLag */
        return NGX_OK;
    }

    if (flags & 0x20) { /* OCRstreamFlag */
        pos += 2;
    }

    if (pos > last) {
        return NGX_ERROR;
    }

    (void) id;

    ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: es descriptor es id=%i flags=%i",
                   (ngx_int_t) id, (ngx_int_t) flags);

    return ngx_rtmp_mp4_parse_descr(s, pos, last);
}


static ngx_int_t
ngx_rtmp_mp4_parse_descr(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    uint8_t                     tag, v;
    uint32_t                    size;
    ngx_uint_t                  n, ndesc;
    ngx_rtmp_mp4_descriptor_t   *ds;

    ndesc = sizeof(ngx_rtmp_mp4_descriptors)
          / sizeof(ngx_rtmp_mp4_descriptors[0]);

    while (pos < last) {
        tag = *(uint8_t *) pos++;

        for (size = 0, n = 0; n < 4; ++n) {
            if (pos == last) {
                return NGX_ERROR;
            }

            v = *(uint8_t *) pos++;

            size = (size << 7) | (v & 0x7f);

            if (!(v & 0x80)) {
                break;
            }
        }

        if (pos + size > last) {
            return NGX_ERROR;
        }

        ds = ngx_rtmp_mp4_descriptors;;

        for (n = 0; n < ndesc; ++n, ++ds) {
            if (tag == ds->tag) {
                break;
            }
        }

        if (n == ndesc) {
            ds = NULL;
        }

        ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                "mp4: descriptor%s tag=%i size=%uD",
                ds ? "" : " unhandled", (ngx_int_t) tag, size);

        if (ds && ds->handler(s, pos, pos + size) != NGX_OK) {
            return NGX_ERROR;
        }

        pos += size;
    }

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_parse_esds(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    if (pos + 4 > last) {
        return NGX_ERROR;
    }

    pos += 4; /* version */

    return ngx_rtmp_mp4_parse_descr(s, pos, last);
}


static ngx_int_t
ngx_rtmp_mp4_parse_mp3(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    return ngx_rtmp_mp4_parse_audio(s, pos, last, NGX_RTMP_AUDIO_MP3);
}


static ngx_int_t
ngx_rtmp_mp4_parse_nmos(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    return ngx_rtmp_mp4_parse_audio(s, pos, last, NGX_RTMP_AUDIO_NELLY);
}


static ngx_int_t
ngx_rtmp_mp4_parse_spex(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    return ngx_rtmp_mp4_parse_audio(s, pos, last, NGX_RTMP_AUDIO_SPEEX);
}


static ngx_int_t
ngx_rtmp_mp4_parse_stsd(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    if (pos + 8 > last) {
        return NGX_ERROR;
    }

    pos += 8;

    ngx_rtmp_mp4_parse(s, pos, last);

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_parse_stsc(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    ngx_rtmp_mp4_ctx_t         *ctx;
    ngx_rtmp_mp4_track_t       *t;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    t = ctx->track;

    if (t == NULL) {
        return NGX_OK;
    }

    t->chunks = (ngx_rtmp_mp4_chunks_t *) pos;

    if (pos + sizeof(*t->chunks) + ngx_rtmp_r32(t->chunks->entry_count) *
                                   sizeof(t->chunks->entries[0])
        <= last)
    {
        ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: chunks entries=%uD",
                       ngx_rtmp_r32(t->chunks->entry_count));
        return NGX_OK;
    }

    t->chunks = NULL;
    return NGX_ERROR;
}


static ngx_int_t
ngx_rtmp_mp4_parse_stts(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    ngx_rtmp_mp4_ctx_t         *ctx;
    ngx_rtmp_mp4_track_t       *t;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    t = ctx->track;

    if (t == NULL) {
        return NGX_OK;
    }

    t->times = (ngx_rtmp_mp4_times_t *) pos;

    if (pos + sizeof(*t->times) + ngx_rtmp_r32(t->times->entry_count) *
                                  sizeof(t->times->entries[0])
        <= last)
    {
        ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: times entries=%uD",
                       ngx_rtmp_r32(t->times->entry_count));
        return NGX_OK;
    }

    t->times = NULL;
    return NGX_ERROR;
}


static ngx_int_t
ngx_rtmp_mp4_parse_ctts(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    ngx_rtmp_mp4_ctx_t         *ctx;
    ngx_rtmp_mp4_track_t       *t;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    t = ctx->track;

    if (t == NULL) {
        return NGX_OK;
    }

    t->delays = (ngx_rtmp_mp4_delays_t *) pos;

    if (pos + sizeof(*t->delays) + ngx_rtmp_r32(t->delays->entry_count) *
                                   sizeof(t->delays->entries[0])
        <= last)
    {
        ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: delays entries=%uD",
                       ngx_rtmp_r32(t->delays->entry_count));
        return NGX_OK;
    }

    t->delays = NULL;
    return NGX_ERROR;
}


static ngx_int_t
ngx_rtmp_mp4_parse_stss(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    ngx_rtmp_mp4_ctx_t         *ctx;
    ngx_rtmp_mp4_track_t       *t;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    t = ctx->track;

    if (t == NULL) {
        return NGX_OK;
    }

    t->keys = (ngx_rtmp_mp4_keys_t *) pos;

    if (pos + sizeof(*t->keys) + ngx_rtmp_r32(t->keys->entry_count) *
                                  sizeof(t->keys->entries[0])
        <= last)
    {
        ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: keys entries=%uD",
                       ngx_rtmp_r32(t->keys->entry_count));
        return NGX_OK;
    }

    t->keys = NULL;
    return NGX_ERROR;
}


static ngx_int_t
ngx_rtmp_mp4_parse_stsz(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    ngx_rtmp_mp4_ctx_t         *ctx;
    ngx_rtmp_mp4_track_t       *t;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    t = ctx->track;

    if (t == NULL) {
        return NGX_OK;
    }

    t->sizes = (ngx_rtmp_mp4_sizes_t *) pos;

    if (pos + sizeof(*t->sizes) <= last && t->sizes->sample_size) {
        ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: sizes size=%uD",
                       ngx_rtmp_r32(t->sizes->sample_size));
        return NGX_OK;
    }

    if (pos + sizeof(*t->sizes) + ngx_rtmp_r32(t->sizes->sample_count) *
                                  sizeof(t->sizes->entries[0])
        <= last)

    {
        ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: sizes entries=%uD",
                       ngx_rtmp_r32(t->sizes->sample_count));
        return NGX_OK;
    }

    t->sizes = NULL;
    return NGX_ERROR;
}


static ngx_int_t
ngx_rtmp_mp4_parse_stz2(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    ngx_rtmp_mp4_ctx_t         *ctx;
    ngx_rtmp_mp4_track_t       *t;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    t = ctx->track;

    if (t == NULL) {
        return NGX_OK;
    }

    t->sizes2 = (ngx_rtmp_mp4_sizes2_t *) pos;

    if (pos + sizeof(*t->sizes) + ngx_rtmp_r32(t->sizes2->sample_count) *
                                  ngx_rtmp_r32(t->sizes2->field_size) / 8
        <= last)
    {
        ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: sizes2 field_size=%uD entries=%uD",
                       ngx_rtmp_r32(t->sizes2->field_size),
                       ngx_rtmp_r32(t->sizes2->sample_count));
        return NGX_OK;
    }

    t->sizes2 = NULL;
    return NGX_ERROR;
}


static ngx_int_t
ngx_rtmp_mp4_parse_stco(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    ngx_rtmp_mp4_ctx_t         *ctx;
    ngx_rtmp_mp4_track_t       *t;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    t = ctx->track;

    if (t == NULL) {
        return NGX_OK;
    }

    t->offsets = (ngx_rtmp_mp4_offsets_t *) pos;

    if (pos + sizeof(*t->offsets) + ngx_rtmp_r32(t->offsets->entry_count) *
                                    sizeof(t->offsets->entries[0])
        <= last)
    {
        ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: offsets entries=%uD",
                       ngx_rtmp_r32(t->offsets->entry_count));
        return NGX_OK;
    }

    t->offsets = NULL;
    return NGX_ERROR;
}


static ngx_int_t
ngx_rtmp_mp4_parse_co64(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    ngx_rtmp_mp4_ctx_t         *ctx;
    ngx_rtmp_mp4_track_t       *t;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    t = ctx->track;

    if (t == NULL) {
        return NGX_OK;
    }

    t->offsets64 = (ngx_rtmp_mp4_offsets64_t *) pos;

    if (pos + sizeof(*t->offsets64) + ngx_rtmp_r32(t->offsets64->entry_count) *
                                      sizeof(t->offsets64->entries[0])
        <= last)
    {
        ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: offsets64 entries=%uD",
                       ngx_rtmp_r32(t->offsets64->entry_count));
        return NGX_OK;
    }

    t->offsets64 = NULL;
    return NGX_ERROR;
}


static ngx_int_t
ngx_rtmp_mp4_parse(ngx_rtmp_session_t *s, u_char *pos, u_char *last)
{
    uint32_t                   *hdr, tag;
    size_t                      size, nboxes;
    ngx_uint_t                  n;
    ngx_rtmp_mp4_box_t         *b;

    while (pos != last) {
        if (pos + 8 > last) {
            ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "mp4: too small box: size=%i", last - pos);
            return NGX_ERROR;
        }

        hdr = (uint32_t *) pos;
        size = ngx_rtmp_r32(hdr[0]);
        if (size == 0) {
            return NGX_ERROR;
        }

        tag  = hdr[1];

        if (pos + size > last) {
            ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno,
                          "mp4: too big box '%*s': size=%uz",
                          4, &tag, size);
            return NGX_ERROR;
        }

        b = ngx_rtmp_mp4_boxes;
        nboxes = sizeof(ngx_rtmp_mp4_boxes) / sizeof(ngx_rtmp_mp4_boxes[0]);

        for (n = 0; n < nboxes && b->tag != tag; ++n, ++b);

        if (n == nboxes) {
            ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "mp4: box unhandled '%*s'", 4, &tag);
        } else {
            ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "mp4: box '%*s'", 4, &tag);
            b->handler(s, pos + 8, pos + size);
        }

        pos += size;
    }

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_next_time(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t)
{
    ngx_rtmp_mp4_cursor_t      *cr;
    ngx_rtmp_mp4_time_entry_t  *te;

    if (t->times == NULL) {
        return NGX_ERROR;
    }

    cr = &t->cursor;

    if (cr->time_pos >= ngx_rtmp_r32(t->times->entry_count)) {
        ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: track#%ui time[%ui/%uD] overflow",
                       t->id, cr->time_pos,
                       ngx_rtmp_r32(t->times->entry_count));

        return NGX_ERROR;
    }

    te = &t->times->entries[cr->time_pos];

    cr->last_timestamp = cr->timestamp;
    cr->timestamp += ngx_rtmp_r32(te->sample_delta);

    cr->not_first = 1;

    ngx_log_debug8(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: track#%ui time[%ui] [%ui/%uD][%ui/%uD]=%uD t=%uD",
                   t->id, cr->pos, cr->time_pos,
                   ngx_rtmp_r32(t->times->entry_count),
                   cr->time_count, ngx_rtmp_r32(te->sample_count),
                   ngx_rtmp_r32(te->sample_delta),
                   cr->timestamp);

    cr->time_count++;
    cr->pos++;

    if (cr->time_count >= ngx_rtmp_r32(te->sample_count)) {
        cr->time_pos++;
        cr->time_count = 0;
    }

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_seek_time(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t,
                       uint32_t timestamp)
{
    ngx_rtmp_mp4_cursor_t      *cr;
    ngx_rtmp_mp4_time_entry_t  *te;
    uint32_t                    dt;

    if (t->times == NULL) {
        return NGX_ERROR;
    }

    cr = &t->cursor;

    te = t->times->entries;

    while (cr->time_pos < ngx_rtmp_r32(t->times->entry_count)) {
        dt = ngx_rtmp_r32(te->sample_delta) * ngx_rtmp_r32(te->sample_count);

        if (cr->timestamp + dt >= timestamp) {
            if (te->sample_delta == 0) {
                return NGX_ERROR;
            }

            cr->time_count = (timestamp - cr->timestamp) /
                             ngx_rtmp_r32(te->sample_delta);
            cr->timestamp += ngx_rtmp_r32(te->sample_delta) * cr->time_count;
            cr->pos += cr->time_count;

            break;
        }

        cr->timestamp += dt;
        cr->pos += ngx_rtmp_r32(te->sample_count);
        cr->time_pos++;
        te++;
    }

    if (cr->time_pos >= ngx_rtmp_r32(t->times->entry_count)) {
        ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: track#%ui seek time[%ui/%uD] overflow",
                       t->id, cr->time_pos,
                       ngx_rtmp_r32(t->times->entry_count));

        return  NGX_ERROR;
    }

    ngx_log_debug8(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: track#%ui seek time[%ui] [%ui/%uD][%ui/%uD]=%uD "
                   "t=%uD",
                   t->id, cr->pos, cr->time_pos,
                   ngx_rtmp_r32(t->times->entry_count),
                   cr->time_count,
                   ngx_rtmp_r32(te->sample_count),
                   ngx_rtmp_r32(te->sample_delta),
                   cr->timestamp);

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_update_offset(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t)
{
    ngx_rtmp_mp4_cursor_t          *cr;
    ngx_uint_t                      chunk;

    cr = &t->cursor;

    if (cr->chunk < 1) {
        ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: track#%ui offset[%ui] underflow",
                       t->id, cr->chunk);
        return NGX_ERROR;
    }

    chunk = cr->chunk - 1;

    if (t->offsets) {
        if (chunk >= ngx_rtmp_r32(t->offsets->entry_count)) {
            ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "mp4: track#%ui offset[%ui/%uD] overflow",
                           t->id, cr->chunk,
                           ngx_rtmp_r32(t->offsets->entry_count));

            return NGX_ERROR;
        }

        cr->offset = (off_t) ngx_rtmp_r32(t->offsets->entries[chunk]);
        cr->size = 0;

        ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: track#%ui offset[%ui/%uD]=%O",
                       t->id, cr->chunk,
                       ngx_rtmp_r32(t->offsets->entry_count),
                       cr->offset);

        return NGX_OK;
    }

    if (t->offsets64) {
        if (chunk >= ngx_rtmp_r32(t->offsets64->entry_count)) {
            ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "mp4: track#%ui offset64[%ui/%uD] overflow",
                           t->id, cr->chunk,
                           ngx_rtmp_r32(t->offsets->entry_count));

            return NGX_ERROR;
        }

        cr->offset = (off_t) ngx_rtmp_r64(t->offsets64->entries[chunk]);
        cr->size = 0;

        ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: track#%ui offset64[%ui/%uD]=%O",
                       t->id, cr->chunk,
                       ngx_rtmp_r32(t->offsets->entry_count),
                       cr->offset);

        return NGX_OK;
    }

    return NGX_ERROR;
}


static ngx_int_t
ngx_rtmp_mp4_next_chunk(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t)
{
    ngx_rtmp_mp4_cursor_t          *cr;
    ngx_rtmp_mp4_chunk_entry_t     *ce, *nce;
    ngx_int_t                       new_chunk;

    if (t->chunks == NULL) {
        return NGX_OK;
    }

    cr = &t->cursor;

    if (cr->chunk_pos >= ngx_rtmp_r32(t->chunks->entry_count)) {
        ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: track#%ui chunk[%ui/%uD] overflow",
                       t->id, cr->chunk_pos,
                       ngx_rtmp_r32(t->chunks->entry_count));

        return NGX_ERROR;
    }

    ce = &t->chunks->entries[cr->chunk_pos];

    cr->chunk_count++;

    if (cr->chunk_count >= ngx_rtmp_r32(ce->samples_per_chunk)) {
        cr->chunk_count = 0;
        cr->chunk++;

        if (cr->chunk_pos + 1 < ngx_rtmp_r32(t->chunks->entry_count)) {
            nce = ce + 1;
            if (cr->chunk >= ngx_rtmp_r32(nce->first_chunk)) {
                cr->chunk_pos++;
                ce = nce;
            }
        }

        new_chunk = 1;

    } else {
        new_chunk = 0;
    }

    ngx_log_debug7(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: track#%ui chunk[%ui/%uD][%uD..%ui][%ui/%uD]",
                   t->id, cr->chunk_pos,
                   ngx_rtmp_r32(t->chunks->entry_count),
                   ngx_rtmp_r32(ce->first_chunk),
                   cr->chunk, cr->chunk_count,
                   ngx_rtmp_r32(ce->samples_per_chunk));


    if (new_chunk) {
        return ngx_rtmp_mp4_update_offset(s, t);
    }

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_seek_chunk(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t)
{
    ngx_rtmp_mp4_cursor_t          *cr;
    ngx_rtmp_mp4_chunk_entry_t     *ce, *nce;
    ngx_uint_t                      pos, dpos, dchunk;

    cr = &t->cursor;

    if (t->chunks == NULL || t->chunks->entry_count == 0) {
        cr->chunk = 1;
        return NGX_OK;
    }

    ce = t->chunks->entries;
    pos = 0;

    while (cr->chunk_pos + 1 < ngx_rtmp_r32(t->chunks->entry_count)) {
        nce = ce + 1;

        dpos = (ngx_rtmp_r32(nce->first_chunk) -
                ngx_rtmp_r32(ce->first_chunk)) *
                ngx_rtmp_r32(ce->samples_per_chunk);

        if (pos + dpos > cr->pos) {
            break;
        }

        pos += dpos;
        ce++;
        cr->chunk_pos++;
    }

    if (ce->samples_per_chunk == 0) {
        return NGX_ERROR;
    }

    dchunk = (cr->pos - pos) / ngx_rtmp_r32(ce->samples_per_chunk);

    cr->chunk = ngx_rtmp_r32(ce->first_chunk) + dchunk;
    cr->chunk_pos = (ngx_uint_t) (ce - t->chunks->entries);
    cr->chunk_count = (ngx_uint_t) (cr->pos - pos - dchunk *
                                    ngx_rtmp_r32(ce->samples_per_chunk));

    ngx_log_debug7(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: track#%ui seek chunk[%ui/%uD][%uD..%ui][%ui/%uD]",
                   t->id, cr->chunk_pos,
                   ngx_rtmp_r32(t->chunks->entry_count),
                   ngx_rtmp_r32(ce->first_chunk),
                   cr->chunk, cr->chunk_count,
                   ngx_rtmp_r32(ce->samples_per_chunk));

    return ngx_rtmp_mp4_update_offset(s, t);
}


static ngx_int_t
ngx_rtmp_mp4_next_size(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t)
{
    ngx_rtmp_mp4_cursor_t          *cr;

    cr = &t->cursor;

    cr->offset += cr->size;

    if (t->sizes) {
        if (t->sizes->sample_size) {
            cr->size = ngx_rtmp_r32(t->sizes->sample_size);

            ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "mp4: track#%ui size fix=%uz",
                           t->id, cr->size);

            return NGX_OK;
        }

        cr->size_pos++;

        if (cr->size_pos >= ngx_rtmp_r32(t->sizes->sample_count)) {
            ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "mp4: track#%ui size[%ui/%uD] overflow",
                           t->id, cr->size_pos,
                           ngx_rtmp_r32(t->sizes->sample_count));

            return NGX_ERROR;
        }

        cr->size = ngx_rtmp_r32(t->sizes->entries[cr->size_pos]);

        ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: track#%ui size[%ui/%uD]=%uz",
                       t->id, cr->size_pos,
                       ngx_rtmp_r32(t->sizes->sample_count),
                       cr->size);

        return NGX_OK;
    }

    if (t->sizes2) {
        if (cr->size_pos >= ngx_rtmp_r32(t->sizes2->sample_count)) {
            ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "mp4: track#%ui size[%ui/%uD] overflow",
                           t->id, cr->size_pos,
                           ngx_rtmp_r32(t->sizes2->sample_count));

            return NGX_ERROR;
        }

        /*TODO*/

        return NGX_OK;
    }

    return NGX_ERROR;
}


static ngx_int_t
ngx_rtmp_mp4_seek_size(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t)
{
    ngx_rtmp_mp4_cursor_t      *cr;
    ngx_uint_t                  pos;

    cr = &t->cursor;

    if (cr->chunk_count > cr->pos) {
        return NGX_ERROR;
    }

    if (t->sizes) {
        if (t->sizes->sample_size) {
            cr->size = ngx_rtmp_r32(t->sizes->sample_size);

            cr->offset += cr->size * cr->chunk_count;

            ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "mp4: track#%ui seek size fix=%uz",
                           t->id, cr->size);

            return NGX_OK;
        }

        if (cr->pos >= ngx_rtmp_r32(t->sizes->sample_count)) {
            ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "mp4: track#%ui seek size[%ui/%uD] overflow",
                           t->id, cr->pos,
                           ngx_rtmp_r32(t->sizes->sample_count));

            return NGX_ERROR;
        }

        for (pos = 1; pos <= cr->chunk_count; ++pos) {
            cr->offset += ngx_rtmp_r32(t->sizes->entries[cr->pos - pos]);
        }

        cr->size_pos = cr->pos;
        cr->size = ngx_rtmp_r32(t->sizes->entries[cr->size_pos]);

        ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: track#%ui seek size[%ui/%uD]=%uz",
                       t->id, cr->size_pos,
                       ngx_rtmp_r32(t->sizes->sample_count),
                       cr->size);

        return NGX_OK;
    }

    if (t->sizes2) {
        if (cr->size_pos >= ngx_rtmp_r32(t->sizes2->sample_count)) {
            ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "mp4: track#%ui seek size2[%ui/%uD] overflow",
                           t->id, cr->size_pos,
                           ngx_rtmp_r32(t->sizes->sample_count));

            return NGX_ERROR;
        }

        cr->size_pos = cr->pos;

        /* TODO */
        return NGX_OK;
    }

    return NGX_ERROR;
}


static ngx_int_t
ngx_rtmp_mp4_next_key(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t)
{
    ngx_rtmp_mp4_cursor_t          *cr;
    uint32_t                       *ke;

    cr = &t->cursor;

    if (t->keys == NULL) {
        return NGX_OK;
    }

    if (cr->key) {
        cr->key_pos++;
    }

    if (cr->key_pos >= ngx_rtmp_r32(t->keys->entry_count)) {
        ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                "mp4: track#%ui key[%ui/%uD] overflow",
                t->id, cr->key_pos,
                ngx_rtmp_r32(t->keys->entry_count));

        cr->key = 0;

        return NGX_OK;
    }

    ke = &t->keys->entries[cr->key_pos];
    cr->key = (cr->pos + 1 == ngx_rtmp_r32(*ke));

    ngx_log_debug6(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: track#%ui key[%ui/%uD][%ui/%uD]=%s",
                   t->id, cr->key_pos,
                   ngx_rtmp_r32(t->keys->entry_count),
                   cr->pos, ngx_rtmp_r32(*ke),
                   cr->key ? "match" : "miss");

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_seek_key(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t)
{
    ngx_rtmp_mp4_cursor_t      *cr;
    uint32_t                   *ke;
    ngx_int_t                   dpos;

    cr = &t->cursor;

    if (t->keys == NULL) {
        return NGX_OK;
    }

    while (cr->key_pos < ngx_rtmp_r32(t->keys->entry_count)) {
        if (ngx_rtmp_r32(t->keys->entries[cr->key_pos]) > cr->pos) {
            break;
        }

        cr->key_pos++;
    }

    if (cr->key_pos >= ngx_rtmp_r32(t->keys->entry_count)) {
        ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                "mp4: track#%ui seek key[%ui/%uD] overflow",
                t->id, cr->key_pos,
                ngx_rtmp_r32(t->keys->entry_count));
        return NGX_OK;
    }

    ke = &t->keys->entries[cr->key_pos];
    /*cr->key = (cr->pos + 1 == ngx_rtmp_r32(*ke));*/

    /* distance to the next keyframe */
    dpos = ngx_rtmp_r32(*ke) - cr->pos - 1;
    cr->key = 1;

    /* TODO: range version needed */
    for (; dpos > 0; --dpos) {
        ngx_rtmp_mp4_next_time(s, t);
    }

/*    cr->key = (cr->pos + 1 == ngx_rtmp_r32(*ke));*/

    ngx_log_debug6(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: track#%ui seek key[%ui/%uD][%ui/%uD]=%s",
                   t->id, cr->key_pos,
                   ngx_rtmp_r32(t->keys->entry_count),
                   cr->pos, ngx_rtmp_r32(*ke),
                   cr->key ? "match" : "miss");

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_next_delay(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t)
{
    ngx_rtmp_mp4_cursor_t          *cr;
    ngx_rtmp_mp4_delay_entry_t     *de;

    cr = &t->cursor;

    if (t->delays == NULL) {
        return NGX_OK;
    }

    if (cr->delay_pos >= ngx_rtmp_r32(t->delays->entry_count)) {
        ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                "mp4: track#%ui delay[%ui/%uD] overflow",
                t->id, cr->delay_pos,
                ngx_rtmp_r32(t->delays->entry_count));

        return NGX_OK;
    }

    cr->delay_count++;
    de = &t->delays->entries[cr->delay_pos];

    if (cr->delay_count >= ngx_rtmp_r32(de->sample_count)) {
        cr->delay_pos++;
        de++;
        cr->delay_count = 0;
    }

    if (cr->delay_pos >= ngx_rtmp_r32(t->delays->entry_count)) {
        ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                "mp4: track#%ui delay[%ui/%uD] overflow",
                t->id, cr->delay_pos,
                ngx_rtmp_r32(t->delays->entry_count));

        return NGX_OK;
    }

    cr->delay = ngx_rtmp_r32(de->sample_offset);

    ngx_log_debug6(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: track#%ui delay[%ui/%uD][%ui/%uD]=%ui",
                   t->id, cr->delay_pos,
                   ngx_rtmp_r32(t->delays->entry_count),
                   cr->delay_count,
                   ngx_rtmp_r32(de->sample_count), cr->delay);

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_seek_delay(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t)
{
    ngx_rtmp_mp4_cursor_t      *cr;
    ngx_rtmp_mp4_delay_entry_t *de;
    uint32_t                    pos, dpos;

    cr = &t->cursor;

    if (t->delays == NULL) {
        return NGX_OK;
    }

    pos = 0;
    de = t->delays->entries;

    while (cr->delay_pos < ngx_rtmp_r32(t->delays->entry_count)) {
        dpos = ngx_rtmp_r32(de->sample_count);

        if (pos + dpos > cr->pos) {
            cr->delay_count = cr->pos - pos;
            cr->delay = ngx_rtmp_r32(de->sample_offset);
            break;
        }

        cr->delay_pos++;
        pos += dpos;
        de++;
    }

    if (cr->delay_pos >= ngx_rtmp_r32(t->delays->entry_count)) {
        ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                "mp4: track#%ui seek delay[%ui/%uD] overflow",
                t->id, cr->delay_pos,
                ngx_rtmp_r32(t->delays->entry_count));

        return NGX_OK;
    }

    ngx_log_debug6(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: track#%ui seek delay[%ui/%uD][%ui/%uD]=%ui",
                   t->id, cr->delay_pos,
                   ngx_rtmp_r32(t->delays->entry_count),
                   cr->delay_count,
                   ngx_rtmp_r32(de->sample_count), cr->delay);

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_next(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t)
{
    if (ngx_rtmp_mp4_next_time(s, t)  != NGX_OK ||
        ngx_rtmp_mp4_next_key(s, t)   != NGX_OK ||
        ngx_rtmp_mp4_next_chunk(s, t) != NGX_OK ||
        ngx_rtmp_mp4_next_size(s, t)  != NGX_OK ||
        ngx_rtmp_mp4_next_delay(s, t) != NGX_OK)
    {
        t->cursor.valid = 0;
        return NGX_ERROR;
    }

    t->cursor.valid = 1;
    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_send_meta(ngx_rtmp_session_t *s)
{
    ngx_rtmp_mp4_ctx_t             *ctx;
    ngx_rtmp_core_srv_conf_t       *cscf;
    ngx_int_t                       rc;
    ngx_uint_t                      n;
    ngx_rtmp_header_t               h;
    ngx_chain_t                    *out;
    ngx_rtmp_mp4_track_t           *t;
    double                          d;

    static struct {
        double                      width;
        double                      height;
        double                      duration;
        double                      video_codec_id;
        double                      audio_codec_id;
        double                      audio_sample_rate;
    }                               v;

    static ngx_rtmp_amf_elt_t       out_inf[] = {

        { NGX_RTMP_AMF_NUMBER,
          ngx_string("width"),
          &v.width, 0 },

        { NGX_RTMP_AMF_NUMBER,
          ngx_string("height"),
          &v.height, 0 },

        { NGX_RTMP_AMF_NUMBER,
          ngx_string("displayWidth"),
          &v.width, 0 },

        { NGX_RTMP_AMF_NUMBER,
          ngx_string("displayHeight"),
          &v.height, 0 },

        { NGX_RTMP_AMF_NUMBER,
          ngx_string("duration"),
          &v.duration, 0 },

        { NGX_RTMP_AMF_NUMBER,
          ngx_string("videocodecid"),
          &v.video_codec_id, 0 },

        { NGX_RTMP_AMF_NUMBER,
          ngx_string("audiocodecid"),
          &v.audio_codec_id, 0 },

        { NGX_RTMP_AMF_NUMBER,
          ngx_string("audiosamplerate"),
          &v.audio_sample_rate, 0 },
    };

    static ngx_rtmp_amf_elt_t       out_elts[] = {

        { NGX_RTMP_AMF_STRING,
          ngx_null_string,
          "onMetaData", 0 },

        { NGX_RTMP_AMF_OBJECT,
          ngx_null_string,
          out_inf, sizeof(out_inf) },
    };

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);
    if (ctx == NULL) {
        return NGX_OK;
    }

    cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module);

    ngx_memzero(&v, sizeof(v));

    v.width  = ctx->width;
    v.height = ctx->height;
    v.audio_sample_rate = ctx->sample_rate;

    t = &ctx->tracks[0];
    for (n = 0; n < ctx->ntracks; ++n, ++t) {
        d = ngx_rtmp_mp4_to_rtmp_timestamp(t, t->duration) / 1000.;

        if (v.duration < d) {
            v.duration = d;
        }

        switch (t->type) {
            case NGX_RTMP_MSG_AUDIO:
                v.audio_codec_id = t->codec;
                break;
            case NGX_RTMP_MSG_VIDEO:
                v.video_codec_id = t->codec;
                break;
        }
    }

    out = NULL;
    rc = ngx_rtmp_append_amf(s, &out, NULL, out_elts,
                             sizeof(out_elts) / sizeof(out_elts[0]));
    if (rc != NGX_OK || out == NULL) {
        return NGX_ERROR;
    }

    ngx_memzero(&h, sizeof(h));

    h.csid = NGX_RTMP_CSID_AMF;
    h.msid = NGX_RTMP_MSID;
    h.type = NGX_RTMP_MSG_AMF_META;

    ngx_rtmp_prepare_message(s, &h, NULL, out);
    rc = ngx_rtmp_send_message(s, out, 0);
    ngx_rtmp_free_shared_chain(cscf, out);

    return rc;
}


static ngx_int_t
ngx_rtmp_mp4_seek_track(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t,
                        ngx_int_t timestamp)
{
    ngx_rtmp_mp4_cursor_t          *cr;

    cr = &t->cursor;
    ngx_memzero(cr, sizeof(*cr));

    if (ngx_rtmp_mp4_seek_time(s, t, ngx_rtmp_mp4_from_rtmp_timestamp(
                          t, timestamp)) != NGX_OK ||
        ngx_rtmp_mp4_seek_key(s, t)   != NGX_OK ||
        ngx_rtmp_mp4_seek_chunk(s, t) != NGX_OK ||
        ngx_rtmp_mp4_seek_size(s, t)  != NGX_OK ||
        ngx_rtmp_mp4_seek_delay(s, t) != NGX_OK)
    {
        return NGX_ERROR;
    }

    cr->valid = 1;
    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_send(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t *ts)
{
    ngx_rtmp_mp4_ctx_t             *ctx;
    ngx_buf_t                       in_buf;
    ngx_rtmp_header_t               h, lh;
    ngx_rtmp_core_srv_conf_t       *cscf;
    ngx_chain_t                    *out, in;
    ngx_rtmp_mp4_track_t           *t, *cur_t;
    ngx_rtmp_mp4_cursor_t          *cr, *cur_cr;
    uint32_t                        buflen, end_timestamp,
                                    timestamp, last_timestamp, rdelay,
                                    cur_timestamp;
    ssize_t                         ret;
    u_char                          fhdr[5];
    size_t                          fhdr_size;
    ngx_int_t                       rc;
    ngx_uint_t                      n, counter;

    cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module);

    ctx  = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    if (ctx == NULL) {
        return NGX_ERROR;
    }

    if (!ctx->meta_sent) {
        rc = ngx_rtmp_mp4_send_meta(s);

        if (rc == NGX_OK) {
            ctx->meta_sent = 1;
        }

        return rc;
    }

    buflen = s->buflen + NGX_RTMP_MP4_BUFLEN_ADDON;

    counter = 0;
    last_timestamp = 0;
    end_timestamp = ctx->start_timestamp +
                    (ngx_current_msec - ctx->epoch) + buflen;

    for ( ;; ) {
        counter++;
        if (counter > NGX_RTMP_MP4_MAX_FRAMES) {
            return NGX_OK;
        }

        timestamp = 0;
        t = NULL;

        for (n = 0; n < ctx->ntracks; n++) {
            cur_t = &ctx->tracks[n];
            cur_cr = &cur_t->cursor;

            if (!cur_cr->valid) {
                continue;
            }

            cur_timestamp = ngx_rtmp_mp4_to_rtmp_timestamp(cur_t,
                                                           cur_cr->timestamp);

            if (t == NULL || cur_timestamp < timestamp) {
                timestamp = cur_timestamp;
                t = cur_t;
            }
        }

        if (t == NULL) {
            ngx_log_error(NGX_LOG_ERR, s->connection->log, 0,
                    "mp4: no track");
            return NGX_DONE;
        }

        if (timestamp > end_timestamp) {
            ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                    "mp4: track#%ui ahead %uD > %uD",
                    t->id, timestamp, end_timestamp);

            if (ts) {
                *ts = last_timestamp;
            }

            return (uint32_t) (timestamp - end_timestamp);
        }

        cr = &t->cursor;

        last_timestamp = ngx_rtmp_mp4_to_rtmp_timestamp(t, cr->last_timestamp);

        ngx_memzero(&h, sizeof(h));

        h.msid = NGX_RTMP_MSID;
        h.type = (uint8_t) t->type;
        h.csid = t->csid;

        lh = h;

        h.timestamp  = timestamp;
        lh.timestamp = last_timestamp;

        ngx_memzero(&in, sizeof(in));
        ngx_memzero(&in_buf, sizeof(in_buf));

        if (t->header && !t->header_sent) {
            ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "mp4: track#%ui sending header of size=%uz",
                           t->id, t->header_size);

            fhdr[0] = t->fhdr;
            fhdr[1] = 0;

            if (t->type == NGX_RTMP_MSG_VIDEO) {
                fhdr[0] |= 0x10;
                fhdr[2] = fhdr[3] = fhdr[4] = 0;
                fhdr_size = 5;
            } else {
                fhdr_size = 2;
            }

            in.buf = &in_buf;
            in_buf.pos  = fhdr;
            in_buf.last = fhdr + fhdr_size;

            out = ngx_rtmp_append_shared_bufs(cscf, NULL, &in);

            in.buf = &in_buf;
            in_buf.pos  = t->header;
            in_buf.last = t->header + t->header_size;

            ngx_rtmp_append_shared_bufs(cscf, out, &in);

            ngx_rtmp_prepare_message(s, &h, NULL, out);
            rc = ngx_rtmp_send_message(s, out, 0);
            ngx_rtmp_free_shared_chain(cscf, out);

            if (rc == NGX_AGAIN) {
                return NGX_AGAIN;
            }

            t->header_sent = 1;
        }

        ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: track#%ui read frame offset=%O, size=%uz, "
                       "timestamp=%uD, last_timestamp=%uD",
                       t->id, cr->offset, cr->size, timestamp,
                       last_timestamp);

        ngx_rtmp_mp4_buffer[0] = t->fhdr;
        fhdr_size = 1;

        if (t->type == NGX_RTMP_MSG_VIDEO) {
            if (cr->key) {
                ngx_rtmp_mp4_buffer[0] |= 0x10;
            } else if (cr->delay) {
                ngx_rtmp_mp4_buffer[0] |= 0x20;
            } else {
                ngx_rtmp_mp4_buffer[0] |= 0x30;
            }

            if (t->header) {
                fhdr_size = 5;

                rdelay = ngx_rtmp_mp4_to_rtmp_timestamp(t, cr->delay);

                ngx_rtmp_mp4_buffer[1] = 1;
                ngx_rtmp_mp4_buffer[2] = (rdelay >> 16) & 0xff;
                ngx_rtmp_mp4_buffer[3] = (rdelay >> 8)  & 0xff;
                ngx_rtmp_mp4_buffer[4] = rdelay & 0xff;
            }

        } else { /* NGX_RTMP_MSG_AUDIO */
            if (t->header) {
                fhdr_size = 2;
                ngx_rtmp_mp4_buffer[1] = 1;
            }
        }

        if (cr->size + fhdr_size > sizeof(ngx_rtmp_mp4_buffer)) {
            ngx_log_error(NGX_LOG_ERR, s->connection->log, 0,
                          "mp4: track#%ui too big frame: %D>%uz",
                          t->id, cr->size, sizeof(ngx_rtmp_mp4_buffer));
            goto next;
        }

        ret = ngx_read_file(f, ngx_rtmp_mp4_buffer + fhdr_size,
                            cr->size, cr->offset);

        if (ret != (ssize_t) cr->size) {
            ngx_log_error(NGX_LOG_ERR, s->connection->log, 0,
                          "mp4: track#%ui could not read frame", t->id);
            goto next;
        }

        in.buf = &in_buf;
        in_buf.pos  = ngx_rtmp_mp4_buffer;
        in_buf.last = ngx_rtmp_mp4_buffer + cr->size + fhdr_size;

        out = ngx_rtmp_append_shared_bufs(cscf, NULL, &in);

        ngx_rtmp_prepare_message(s, &h, cr->not_first ? &lh : NULL, out);
        rc = ngx_rtmp_send_message(s, out, 0);
        ngx_rtmp_free_shared_chain(cscf, out);

        if (rc == NGX_AGAIN) {
            return NGX_AGAIN;
        }

        s->current_time = timestamp;

next:
        if (ngx_rtmp_mp4_next(s, t) != NGX_OK) {
            return NGX_DONE;
        }
    }
}


static ngx_int_t
ngx_rtmp_mp4_init(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_int_t aindex,
                  ngx_int_t vindex)
{
    ngx_rtmp_mp4_ctx_t         *ctx;
    uint32_t                    hdr[2];
    ssize_t                     n;
    size_t                      offset, page_offset, size, shift;
    uint64_t                    extended_size;
    ngx_file_info_t             fi;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    if (ctx == NULL) {
        ctx = ngx_palloc(s->connection->pool, sizeof(ngx_rtmp_mp4_ctx_t));

        if (ctx == NULL) {
            return NGX_ERROR;
        }

        ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_mp4_module);
    }

    ngx_memzero(ctx, sizeof(*ctx));

    ctx->aindex = aindex;
    ctx->vindex = vindex;

    offset = 0;
    size   = 0;

    for ( ;; ) {
        n = ngx_read_file(f, (u_char *) &hdr, sizeof(hdr), offset);

        if (n != sizeof(hdr)) {
            ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno,
                          "mp4: error reading file at offset=%uz "
                          "while searching for moov box", offset);
            return NGX_ERROR;
        }

        size = (size_t) ngx_rtmp_r32(hdr[0]);
        shift = sizeof(hdr);

        if (size == 1) {
            n = ngx_read_file(f, (u_char *) &extended_size,
                              sizeof(extended_size), offset + sizeof(hdr));

            if (n != sizeof(extended_size)) {
                ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno,
                              "mp4: error reading file at offset=%uz "
                              "while searching for moov box", offset + 8);
                return NGX_ERROR;
            }

            size = (size_t) ngx_rtmp_r64(extended_size);
            shift += sizeof(extended_size);

        } else if (size == 0) {
            if (ngx_fd_info(f->fd, &fi) == NGX_FILE_ERROR) {
                ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno,
                              "mp4: " ngx_fd_info_n " failed");
                return NGX_ERROR;
            }
            size = ngx_file_size(&fi) - offset;
        }

        if (hdr[1] == ngx_rtmp_mp4_make_tag('m','o','o','v')) {
            ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                           "mp4: found moov box");
            break;
        }

        ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: skipping box '%*s'", 4, hdr + 1);

        offset += size;
    }

    if (size < shift) {
        return NGX_ERROR;
    }

    size   -= shift;
    offset += shift;

    page_offset = offset & (ngx_pagesize - 1);
    ctx->mmaped_size = page_offset + size;

    ctx->mmaped = ngx_rtmp_mp4_mmap(f->fd, ctx->mmaped_size,
                                    offset - page_offset, &ctx->extra);
    if (ctx->mmaped == NULL) {
        ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno,
                      "mp4: mmap failed at offset=%ui, size=%uz",
                      offset, size);
        return NGX_ERROR;
    }

    return ngx_rtmp_mp4_parse(s, (u_char *) ctx->mmaped + page_offset,
                                 (u_char *) ctx->mmaped + page_offset + size);
}


static ngx_int_t
ngx_rtmp_mp4_done(ngx_rtmp_session_t *s, ngx_file_t *f)
{
    ngx_rtmp_mp4_ctx_t            *ctx;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    if (ctx == NULL || ctx->mmaped == NULL) {
        return NGX_OK;
    }

    if (ngx_rtmp_mp4_munmap(ctx->mmaped, ctx->mmaped_size, &ctx->extra)
        != NGX_OK)
    {
        ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno,
                      "mp4: munmap failed");
        return NGX_ERROR;
    }

    ctx->mmaped = NULL;
    ctx->mmaped_size = 0;

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_seek(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t timestamp)
{
    ngx_rtmp_mp4_ctx_t     *ctx;
    ngx_rtmp_mp4_track_t   *t;
    ngx_uint_t              n;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    if (ctx == NULL) {
        return NGX_OK;
    }

    ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: seek timestamp=%ui", timestamp);

    for (n = 0; n < ctx->ntracks; ++n) {
        t = &ctx->tracks[n];

        if (t->type != NGX_RTMP_MSG_VIDEO) {
            continue;
        }

        ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: track#%ui seek video", n);

        ngx_rtmp_mp4_seek_track(s, t, timestamp);

        timestamp = ngx_rtmp_mp4_to_rtmp_timestamp(t, t->cursor.timestamp);

        break;
    }

    for (n = 0; n < ctx->ntracks; ++n) {
        t = &ctx->tracks[n];

        if (t->type == NGX_RTMP_MSG_VIDEO) {
            continue;
        }

        ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                       "mp4: track#%ui seek", n);

        ngx_rtmp_mp4_seek_track(s, &ctx->tracks[n], timestamp);
    }

    ctx->start_timestamp = timestamp;
    ctx->epoch = ngx_current_msec;

    return ngx_rtmp_mp4_reset(s);
}


static ngx_int_t
ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_file_t *f)
{
    ngx_rtmp_mp4_ctx_t     *ctx;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    if (ctx == NULL) {
        return NGX_OK;
    }

    ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: start timestamp=%uD", ctx->start_timestamp);

    ctx->epoch = ngx_current_msec;

    return NGX_OK;/*ngx_rtmp_mp4_reset(s);*/
}


static ngx_int_t
ngx_rtmp_mp4_reset(ngx_rtmp_session_t *s)
{
    ngx_rtmp_mp4_ctx_t     *ctx;
    ngx_rtmp_mp4_cursor_t  *cr;
    ngx_rtmp_mp4_track_t   *t;
    ngx_uint_t              n;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    if (ctx == NULL) {
        return NGX_OK;
    }

    t = &ctx->tracks[0];
    for (n = 0; n < ctx->ntracks; ++n, ++t) {
        cr = &t->cursor;
        cr->not_first = 0;
    }

    return NGX_OK;
}


static ngx_int_t
ngx_rtmp_mp4_stop(ngx_rtmp_session_t *s, ngx_file_t *f)
{
    ngx_rtmp_mp4_ctx_t     *ctx;

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module);

    if (ctx == NULL) {
        return NGX_OK;
    }

    ctx->start_timestamp += (ngx_current_msec - ctx->epoch);

    ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
                   "mp4: stop timestamp=%uD", ctx->start_timestamp);

    return NGX_OK;/*ngx_rtmp_mp4_reset(s);*/
}


static ngx_int_t
ngx_rtmp_mp4_postconfiguration(ngx_conf_t *cf)
{
    ngx_rtmp_play_main_conf_t      *pmcf;
    ngx_rtmp_play_fmt_t           **pfmt, *fmt;

    pmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_play_module);

    pfmt = ngx_array_push(&pmcf->fmts);

    if (pfmt == NULL) {
        return NGX_ERROR;
    }

    fmt = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_play_fmt_t));

    if (fmt == NULL) {
        return NGX_ERROR;
    }

    *pfmt = fmt;

    ngx_str_set(&fmt->name, "mp4-format");

    ngx_str_set(&fmt->pfx, "mp4:");
    ngx_str_set(&fmt->sfx, ".mp4");

    fmt->init  = ngx_rtmp_mp4_init;
    fmt->done  = ngx_rtmp_mp4_done;
    fmt->seek  = ngx_rtmp_mp4_seek;
    fmt->start = ngx_rtmp_mp4_start;
    fmt->stop  = ngx_rtmp_mp4_stop;
    fmt->send  = ngx_rtmp_mp4_send;

    return NGX_OK;
}