view src/http/modules/ngx_http_mp4_module.c @ 7945:f17ba8ecaaf0

Mp4: mp4_start_key_frame directive. The directive enables including all frames from start time to the most recent key frame in the result. Those frames are removed from presentation timeline using mp4 edit lists. Edit lists are currently supported by popular players and browsers such as Chrome, Safari, QuickTime and ffmpeg. Among those not supporting them properly is Firefox[1]. Based on a patch by Tracey Jaquith, Internet Archive. [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1735300
author Roman Arutyunyan <arut@nginx.com>
date Thu, 28 Oct 2021 14:14:25 +0300
parents 24f7904dbfa0
children 1afd19dc7161 adae1da17749
line wrap: on
line source


/*
 * Copyright (C) Igor Sysoev
 * Copyright (C) Nginx, Inc.
 */

#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>


#define NGX_HTTP_MP4_TRAK_ATOM     0
#define NGX_HTTP_MP4_TKHD_ATOM     1
#define NGX_HTTP_MP4_EDTS_ATOM     2
#define NGX_HTTP_MP4_ELST_ATOM     3
#define NGX_HTTP_MP4_MDIA_ATOM     4
#define NGX_HTTP_MP4_MDHD_ATOM     5
#define NGX_HTTP_MP4_HDLR_ATOM     6
#define NGX_HTTP_MP4_MINF_ATOM     7
#define NGX_HTTP_MP4_VMHD_ATOM     8
#define NGX_HTTP_MP4_SMHD_ATOM     9
#define NGX_HTTP_MP4_DINF_ATOM    10
#define NGX_HTTP_MP4_STBL_ATOM    11
#define NGX_HTTP_MP4_STSD_ATOM    12
#define NGX_HTTP_MP4_STTS_ATOM    13
#define NGX_HTTP_MP4_STTS_DATA    14
#define NGX_HTTP_MP4_STSS_ATOM    15
#define NGX_HTTP_MP4_STSS_DATA    16
#define NGX_HTTP_MP4_CTTS_ATOM    17
#define NGX_HTTP_MP4_CTTS_DATA    18
#define NGX_HTTP_MP4_STSC_ATOM    19
#define NGX_HTTP_MP4_STSC_START   20
#define NGX_HTTP_MP4_STSC_DATA    21
#define NGX_HTTP_MP4_STSC_END     22
#define NGX_HTTP_MP4_STSZ_ATOM    23
#define NGX_HTTP_MP4_STSZ_DATA    24
#define NGX_HTTP_MP4_STCO_ATOM    25
#define NGX_HTTP_MP4_STCO_DATA    26
#define NGX_HTTP_MP4_CO64_ATOM    27
#define NGX_HTTP_MP4_CO64_DATA    28

#define NGX_HTTP_MP4_LAST_ATOM    NGX_HTTP_MP4_CO64_DATA


typedef struct {
    size_t                buffer_size;
    size_t                max_buffer_size;
    ngx_flag_t            start_key_frame;
} ngx_http_mp4_conf_t;


typedef struct {
    u_char                chunk[4];
    u_char                samples[4];
    u_char                id[4];
} ngx_mp4_stsc_entry_t;


typedef struct {
    u_char                size[4];
    u_char                name[4];
} ngx_mp4_edts_atom_t;


typedef struct {
    u_char                size[4];
    u_char                name[4];
    u_char                version[1];
    u_char                flags[3];
    u_char                entries[4];
    u_char                duration[8];
    u_char                media_time[8];
    u_char                media_rate[2];
    u_char                reserved[2];
} ngx_mp4_elst_atom_t;


typedef struct {
    uint32_t              timescale;
    uint32_t              time_to_sample_entries;
    uint32_t              sample_to_chunk_entries;
    uint32_t              sync_samples_entries;
    uint32_t              composition_offset_entries;
    uint32_t              sample_sizes_entries;
    uint32_t              chunks;

    ngx_uint_t            start_sample;
    ngx_uint_t            end_sample;
    ngx_uint_t            start_chunk;
    ngx_uint_t            end_chunk;
    ngx_uint_t            start_chunk_samples;
    ngx_uint_t            end_chunk_samples;
    uint64_t              start_chunk_samples_size;
    uint64_t              end_chunk_samples_size;
    uint64_t              duration;
    uint64_t              prefix;
    uint64_t              movie_duration;
    off_t                 start_offset;
    off_t                 end_offset;

    size_t                tkhd_size;
    size_t                mdhd_size;
    size_t                hdlr_size;
    size_t                vmhd_size;
    size_t                smhd_size;
    size_t                dinf_size;
    size_t                size;

    ngx_chain_t           out[NGX_HTTP_MP4_LAST_ATOM + 1];

    ngx_buf_t             trak_atom_buf;
    ngx_buf_t             tkhd_atom_buf;
    ngx_buf_t             edts_atom_buf;
    ngx_buf_t             elst_atom_buf;
    ngx_buf_t             mdia_atom_buf;
    ngx_buf_t             mdhd_atom_buf;
    ngx_buf_t             hdlr_atom_buf;
    ngx_buf_t             minf_atom_buf;
    ngx_buf_t             vmhd_atom_buf;
    ngx_buf_t             smhd_atom_buf;
    ngx_buf_t             dinf_atom_buf;
    ngx_buf_t             stbl_atom_buf;
    ngx_buf_t             stsd_atom_buf;
    ngx_buf_t             stts_atom_buf;
    ngx_buf_t             stts_data_buf;
    ngx_buf_t             stss_atom_buf;
    ngx_buf_t             stss_data_buf;
    ngx_buf_t             ctts_atom_buf;
    ngx_buf_t             ctts_data_buf;
    ngx_buf_t             stsc_atom_buf;
    ngx_buf_t             stsc_start_chunk_buf;
    ngx_buf_t             stsc_end_chunk_buf;
    ngx_buf_t             stsc_data_buf;
    ngx_buf_t             stsz_atom_buf;
    ngx_buf_t             stsz_data_buf;
    ngx_buf_t             stco_atom_buf;
    ngx_buf_t             stco_data_buf;
    ngx_buf_t             co64_atom_buf;
    ngx_buf_t             co64_data_buf;

    ngx_mp4_edts_atom_t   edts_atom;
    ngx_mp4_elst_atom_t   elst_atom;
    ngx_mp4_stsc_entry_t  stsc_start_chunk_entry;
    ngx_mp4_stsc_entry_t  stsc_end_chunk_entry;
} ngx_http_mp4_trak_t;


typedef struct {
    ngx_file_t            file;

    u_char               *buffer;
    u_char               *buffer_start;
    u_char               *buffer_pos;
    u_char               *buffer_end;
    size_t                buffer_size;

    off_t                 offset;
    off_t                 end;
    off_t                 content_length;
    ngx_uint_t            start;
    ngx_uint_t            length;
    uint32_t              timescale;
    ngx_http_request_t   *request;
    ngx_array_t           trak;
    ngx_http_mp4_trak_t   traks[2];

    size_t                ftyp_size;
    size_t                moov_size;

    ngx_chain_t          *out;
    ngx_chain_t           ftyp_atom;
    ngx_chain_t           moov_atom;
    ngx_chain_t           mvhd_atom;
    ngx_chain_t           mdat_atom;
    ngx_chain_t           mdat_data;

    ngx_buf_t             ftyp_atom_buf;
    ngx_buf_t             moov_atom_buf;
    ngx_buf_t             mvhd_atom_buf;
    ngx_buf_t             mdat_atom_buf;
    ngx_buf_t             mdat_data_buf;

    u_char                moov_atom_header[8];
    u_char                mdat_atom_header[16];
} ngx_http_mp4_file_t;


typedef struct {
    char                 *name;
    ngx_int_t           (*handler)(ngx_http_mp4_file_t *mp4,
                                   uint64_t atom_data_size);
} ngx_http_mp4_atom_handler_t;


#define ngx_mp4_atom_header(mp4)   (mp4->buffer_pos - 8)
#define ngx_mp4_atom_data(mp4)     mp4->buffer_pos
#define ngx_mp4_atom_data_size(t)  (uint64_t) (sizeof(t) - 8)


#define ngx_mp4_atom_next(mp4, n)                                             \
                                                                              \
    if (n > (size_t) (mp4->buffer_end - mp4->buffer_pos)) {                   \
        mp4->buffer_pos = mp4->buffer_end;                                    \
                                                                              \
    } else {                                                                  \
        mp4->buffer_pos += (size_t) n;                                        \
    }                                                                         \
                                                                              \
    mp4->offset += n


#define ngx_mp4_set_atom_name(p, n1, n2, n3, n4)                              \
    ((u_char *) (p))[4] = n1;                                                 \
    ((u_char *) (p))[5] = n2;                                                 \
    ((u_char *) (p))[6] = n3;                                                 \
    ((u_char *) (p))[7] = n4

#define ngx_mp4_get_16value(p)                                                \
    ( ((uint16_t) ((u_char *) (p))[0] << 8)                                   \
    + (           ((u_char *) (p))[1]) )

#define ngx_mp4_set_16value(p, n)                                             \
    ((u_char *) (p))[0] = (u_char) ((n) >> 8);                                \
    ((u_char *) (p))[1] = (u_char)  (n)

#define ngx_mp4_get_32value(p)                                                \
    ( ((uint32_t) ((u_char *) (p))[0] << 24)                                  \
    + (           ((u_char *) (p))[1] << 16)                                  \
    + (           ((u_char *) (p))[2] << 8)                                   \
    + (           ((u_char *) (p))[3]) )

#define ngx_mp4_set_32value(p, n)                                             \
    ((u_char *) (p))[0] = (u_char) ((n) >> 24);                               \
    ((u_char *) (p))[1] = (u_char) ((n) >> 16);                               \
    ((u_char *) (p))[2] = (u_char) ((n) >> 8);                                \
    ((u_char *) (p))[3] = (u_char)  (n)

#define ngx_mp4_get_64value(p)                                                \
    ( ((uint64_t) ((u_char *) (p))[0] << 56)                                  \
    + ((uint64_t) ((u_char *) (p))[1] << 48)                                  \
    + ((uint64_t) ((u_char *) (p))[2] << 40)                                  \
    + ((uint64_t) ((u_char *) (p))[3] << 32)                                  \
    + ((uint64_t) ((u_char *) (p))[4] << 24)                                  \
    + (           ((u_char *) (p))[5] << 16)                                  \
    + (           ((u_char *) (p))[6] << 8)                                   \
    + (           ((u_char *) (p))[7]) )

#define ngx_mp4_set_64value(p, n)                                             \
    ((u_char *) (p))[0] = (u_char) ((uint64_t) (n) >> 56);                    \
    ((u_char *) (p))[1] = (u_char) ((uint64_t) (n) >> 48);                    \
    ((u_char *) (p))[2] = (u_char) ((uint64_t) (n) >> 40);                    \
    ((u_char *) (p))[3] = (u_char) ((uint64_t) (n) >> 32);                    \
    ((u_char *) (p))[4] = (u_char) (           (n) >> 24);                    \
    ((u_char *) (p))[5] = (u_char) (           (n) >> 16);                    \
    ((u_char *) (p))[6] = (u_char) (           (n) >> 8);                     \
    ((u_char *) (p))[7] = (u_char)             (n)

#define ngx_mp4_last_trak(mp4)                                                \
    &((ngx_http_mp4_trak_t *) mp4->trak.elts)[mp4->trak.nelts - 1]


static ngx_int_t ngx_http_mp4_handler(ngx_http_request_t *r);
static ngx_int_t ngx_http_mp4_atofp(u_char *line, size_t n, size_t point);

static ngx_int_t ngx_http_mp4_process(ngx_http_mp4_file_t *mp4);
static ngx_int_t ngx_http_mp4_read_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_atom_handler_t *atom, uint64_t atom_data_size);
static ngx_int_t ngx_http_mp4_read(ngx_http_mp4_file_t *mp4, size_t size);
static ngx_int_t ngx_http_mp4_read_ftyp_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static ngx_int_t ngx_http_mp4_read_moov_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static ngx_int_t ngx_http_mp4_read_mdat_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static size_t ngx_http_mp4_update_mdat_atom(ngx_http_mp4_file_t *mp4,
    off_t start_offset, off_t end_offset);
static ngx_int_t ngx_http_mp4_read_mvhd_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static ngx_int_t ngx_http_mp4_read_trak_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static void ngx_http_mp4_update_trak_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak);
static ngx_int_t ngx_http_mp4_read_cmov_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static ngx_int_t ngx_http_mp4_read_tkhd_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static ngx_int_t ngx_http_mp4_read_mdia_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static void ngx_http_mp4_update_mdia_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak);
static ngx_int_t ngx_http_mp4_read_mdhd_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static void ngx_http_mp4_update_mdhd_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak);
static ngx_int_t ngx_http_mp4_read_hdlr_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static ngx_int_t ngx_http_mp4_read_minf_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static void ngx_http_mp4_update_minf_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak);
static ngx_int_t ngx_http_mp4_read_dinf_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static ngx_int_t ngx_http_mp4_read_vmhd_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static ngx_int_t ngx_http_mp4_read_smhd_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static ngx_int_t ngx_http_mp4_read_stbl_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static void ngx_http_mp4_update_edts_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak);
static void ngx_http_mp4_update_stbl_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak);
static ngx_int_t ngx_http_mp4_read_stsd_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static ngx_int_t ngx_http_mp4_read_stts_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static ngx_int_t ngx_http_mp4_update_stts_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak);
static ngx_int_t ngx_http_mp4_crop_stts_data(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak, ngx_uint_t start);
static uint32_t ngx_http_mp4_seek_key_frame(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak, uint32_t start_sample);
static ngx_int_t ngx_http_mp4_read_stss_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static ngx_int_t ngx_http_mp4_update_stss_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak);
static void ngx_http_mp4_crop_stss_data(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak, ngx_uint_t start);
static ngx_int_t ngx_http_mp4_read_ctts_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static void ngx_http_mp4_update_ctts_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak);
static void ngx_http_mp4_crop_ctts_data(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak, ngx_uint_t start);
static ngx_int_t ngx_http_mp4_read_stsc_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static ngx_int_t ngx_http_mp4_update_stsc_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak);
static ngx_int_t ngx_http_mp4_crop_stsc_data(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak, ngx_uint_t start);
static ngx_int_t ngx_http_mp4_read_stsz_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static ngx_int_t ngx_http_mp4_update_stsz_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak);
static ngx_int_t ngx_http_mp4_read_stco_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static ngx_int_t ngx_http_mp4_update_stco_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak);
static void ngx_http_mp4_adjust_stco_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak, int32_t adjustment);
static ngx_int_t ngx_http_mp4_read_co64_atom(ngx_http_mp4_file_t *mp4,
    uint64_t atom_data_size);
static ngx_int_t ngx_http_mp4_update_co64_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak);
static void ngx_http_mp4_adjust_co64_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak, off_t adjustment);

static char *ngx_http_mp4(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static void *ngx_http_mp4_create_conf(ngx_conf_t *cf);
static char *ngx_http_mp4_merge_conf(ngx_conf_t *cf, void *parent, void *child);


static ngx_command_t  ngx_http_mp4_commands[] = {

    { ngx_string("mp4"),
      NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS,
      ngx_http_mp4,
      0,
      0,
      NULL },

    { ngx_string("mp4_buffer_size"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_size_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_mp4_conf_t, buffer_size),
      NULL },

    { ngx_string("mp4_max_buffer_size"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_size_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_mp4_conf_t, max_buffer_size),
      NULL },

    { ngx_string("mp4_start_key_frame"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
      ngx_conf_set_flag_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_mp4_conf_t, start_key_frame),
      NULL },

      ngx_null_command
};


static ngx_http_module_t  ngx_http_mp4_module_ctx = {
    NULL,                          /* preconfiguration */
    NULL,                          /* postconfiguration */

    NULL,                          /* create main configuration */
    NULL,                          /* init main configuration */

    NULL,                          /* create server configuration */
    NULL,                          /* merge server configuration */

    ngx_http_mp4_create_conf,      /* create location configuration */
    ngx_http_mp4_merge_conf        /* merge location configuration */
};


ngx_module_t  ngx_http_mp4_module = {
    NGX_MODULE_V1,
    &ngx_http_mp4_module_ctx,      /* module context */
    ngx_http_mp4_commands,         /* module directives */
    NGX_HTTP_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_http_mp4_atom_handler_t  ngx_http_mp4_atoms[] = {
    { "ftyp", ngx_http_mp4_read_ftyp_atom },
    { "moov", ngx_http_mp4_read_moov_atom },
    { "mdat", ngx_http_mp4_read_mdat_atom },
    { NULL, NULL }
};

static ngx_http_mp4_atom_handler_t  ngx_http_mp4_moov_atoms[] = {
    { "mvhd", ngx_http_mp4_read_mvhd_atom },
    { "trak", ngx_http_mp4_read_trak_atom },
    { "cmov", ngx_http_mp4_read_cmov_atom },
    { NULL, NULL }
};

static ngx_http_mp4_atom_handler_t  ngx_http_mp4_trak_atoms[] = {
    { "tkhd", ngx_http_mp4_read_tkhd_atom },
    { "mdia", ngx_http_mp4_read_mdia_atom },
    { NULL, NULL }
};

static ngx_http_mp4_atom_handler_t  ngx_http_mp4_mdia_atoms[] = {
    { "mdhd", ngx_http_mp4_read_mdhd_atom },
    { "hdlr", ngx_http_mp4_read_hdlr_atom },
    { "minf", ngx_http_mp4_read_minf_atom },
    { NULL, NULL }
};

static ngx_http_mp4_atom_handler_t  ngx_http_mp4_minf_atoms[] = {
    { "vmhd", ngx_http_mp4_read_vmhd_atom },
    { "smhd", ngx_http_mp4_read_smhd_atom },
    { "dinf", ngx_http_mp4_read_dinf_atom },
    { "stbl", ngx_http_mp4_read_stbl_atom },
    { NULL, NULL }
};

static ngx_http_mp4_atom_handler_t  ngx_http_mp4_stbl_atoms[] = {
    { "stsd", ngx_http_mp4_read_stsd_atom },
    { "stts", ngx_http_mp4_read_stts_atom },
    { "stss", ngx_http_mp4_read_stss_atom },
    { "ctts", ngx_http_mp4_read_ctts_atom },
    { "stsc", ngx_http_mp4_read_stsc_atom },
    { "stsz", ngx_http_mp4_read_stsz_atom },
    { "stco", ngx_http_mp4_read_stco_atom },
    { "co64", ngx_http_mp4_read_co64_atom },
    { NULL, NULL }
};


static ngx_int_t
ngx_http_mp4_handler(ngx_http_request_t *r)
{
    u_char                    *last;
    size_t                     root;
    ngx_int_t                  rc, start, end;
    ngx_uint_t                 level, length;
    ngx_str_t                  path, value;
    ngx_log_t                 *log;
    ngx_buf_t                 *b;
    ngx_chain_t                out;
    ngx_http_mp4_file_t       *mp4;
    ngx_open_file_info_t       of;
    ngx_http_core_loc_conf_t  *clcf;

    if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) {
        return NGX_HTTP_NOT_ALLOWED;
    }

    if (r->uri.data[r->uri.len - 1] == '/') {
        return NGX_DECLINED;
    }

    rc = ngx_http_discard_request_body(r);

    if (rc != NGX_OK) {
        return rc;
    }

    last = ngx_http_map_uri_to_path(r, &path, &root, 0);
    if (last == NULL) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    log = r->connection->log;

    path.len = last - path.data;

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0,
                   "http mp4 filename: \"%V\"", &path);

    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);

    ngx_memzero(&of, sizeof(ngx_open_file_info_t));

    of.read_ahead = clcf->read_ahead;
    of.directio = NGX_MAX_OFF_T_VALUE;
    of.valid = clcf->open_file_cache_valid;
    of.min_uses = clcf->open_file_cache_min_uses;
    of.errors = clcf->open_file_cache_errors;
    of.events = clcf->open_file_cache_events;

    if (ngx_http_set_disable_symlinks(r, clcf, &path, &of) != NGX_OK) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    if (ngx_open_cached_file(clcf->open_file_cache, &path, &of, r->pool)
        != NGX_OK)
    {
        switch (of.err) {

        case 0:
            return NGX_HTTP_INTERNAL_SERVER_ERROR;

        case NGX_ENOENT:
        case NGX_ENOTDIR:
        case NGX_ENAMETOOLONG:

            level = NGX_LOG_ERR;
            rc = NGX_HTTP_NOT_FOUND;
            break;

        case NGX_EACCES:
#if (NGX_HAVE_OPENAT)
        case NGX_EMLINK:
        case NGX_ELOOP:
#endif

            level = NGX_LOG_ERR;
            rc = NGX_HTTP_FORBIDDEN;
            break;

        default:

            level = NGX_LOG_CRIT;
            rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
            break;
        }

        if (rc != NGX_HTTP_NOT_FOUND || clcf->log_not_found) {
            ngx_log_error(level, log, of.err,
                          "%s \"%s\" failed", of.failed, path.data);
        }

        return rc;
    }

    if (!of.is_file) {
        return NGX_DECLINED;
    }

    r->root_tested = !r->error_page;
    r->allow_ranges = 1;

    start = -1;
    length = 0;
    r->headers_out.content_length_n = of.size;
    mp4 = NULL;
    b = NULL;

    if (r->args.len) {

        if (ngx_http_arg(r, (u_char *) "start", 5, &value) == NGX_OK) {

            /*
             * A Flash player may send start value with a lot of digits
             * after dot so a custom function is used instead of ngx_atofp().
             */

            start = ngx_http_mp4_atofp(value.data, value.len, 3);
        }

        if (ngx_http_arg(r, (u_char *) "end", 3, &value) == NGX_OK) {

            end = ngx_http_mp4_atofp(value.data, value.len, 3);

            if (end > 0) {
                if (start < 0) {
                    start = 0;
                }

                if (end > start) {
                    length = end - start;
                }
            }
        }
    }

    if (start >= 0) {
        r->single_range = 1;

        mp4 = ngx_pcalloc(r->pool, sizeof(ngx_http_mp4_file_t));
        if (mp4 == NULL) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        mp4->file.fd = of.fd;
        mp4->file.name = path;
        mp4->file.log = r->connection->log;
        mp4->end = of.size;
        mp4->start = (ngx_uint_t) start;
        mp4->length = length;
        mp4->request = r;

        switch (ngx_http_mp4_process(mp4)) {

        case NGX_DECLINED:
            if (mp4->buffer) {
                ngx_pfree(r->pool, mp4->buffer);
            }

            ngx_pfree(r->pool, mp4);
            mp4 = NULL;

            break;

        case NGX_OK:
            r->headers_out.content_length_n = mp4->content_length;
            break;

        default: /* NGX_ERROR */
            if (mp4->buffer) {
                ngx_pfree(r->pool, mp4->buffer);
            }

            ngx_pfree(r->pool, mp4);

            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }
    }

    log->action = "sending mp4 to client";

    if (clcf->directio <= of.size) {

        /*
         * DIRECTIO is set on transfer only
         * to allow kernel to cache "moov" atom
         */

        if (ngx_directio_on(of.fd) == NGX_FILE_ERROR) {
            ngx_log_error(NGX_LOG_ALERT, log, ngx_errno,
                          ngx_directio_on_n " \"%s\" failed", path.data);
        }

        of.is_directio = 1;

        if (mp4) {
            mp4->file.directio = 1;
        }
    }

    r->headers_out.status = NGX_HTTP_OK;
    r->headers_out.last_modified_time = of.mtime;

    if (ngx_http_set_etag(r) != NGX_OK) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    if (ngx_http_set_content_type(r) != NGX_OK) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    if (mp4 == NULL) {
        b = ngx_calloc_buf(r->pool);
        if (b == NULL) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        b->file = ngx_pcalloc(r->pool, sizeof(ngx_file_t));
        if (b->file == NULL) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }
    }

    rc = ngx_http_send_header(r);

    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
        return rc;
    }

    if (mp4) {
        return ngx_http_output_filter(r, mp4->out);
    }

    b->file_pos = 0;
    b->file_last = of.size;

    b->in_file = b->file_last ? 1 : 0;
    b->last_buf = (r == r->main) ? 1 : 0;
    b->last_in_chain = 1;

    b->file->fd = of.fd;
    b->file->name = path;
    b->file->log = log;
    b->file->directio = of.is_directio;

    out.buf = b;
    out.next = NULL;

    return ngx_http_output_filter(r, &out);
}


static ngx_int_t
ngx_http_mp4_atofp(u_char *line, size_t n, size_t point)
{
    ngx_int_t   value, cutoff, cutlim;
    ngx_uint_t  dot;

    /* same as ngx_atofp(), but allows additional digits */

    if (n == 0) {
        return NGX_ERROR;
    }

    cutoff = NGX_MAX_INT_T_VALUE / 10;
    cutlim = NGX_MAX_INT_T_VALUE % 10;

    dot = 0;

    for (value = 0; n--; line++) {

        if (*line == '.') {
            if (dot) {
                return NGX_ERROR;
            }

            dot = 1;
            continue;
        }

        if (*line < '0' || *line > '9') {
            return NGX_ERROR;
        }

        if (point == 0) {
            continue;
        }

        if (value >= cutoff && (value > cutoff || *line - '0' > cutlim)) {
            return NGX_ERROR;
        }

        value = value * 10 + (*line - '0');
        point -= dot;
    }

    while (point--) {
        if (value > cutoff) {
            return NGX_ERROR;
        }

        value = value * 10;
    }

    return value;
}


static ngx_int_t
ngx_http_mp4_process(ngx_http_mp4_file_t *mp4)
{
    off_t                  start_offset, end_offset, adjustment;
    ngx_int_t              rc;
    ngx_uint_t             i, j;
    ngx_chain_t          **prev;
    ngx_http_mp4_trak_t   *trak;
    ngx_http_mp4_conf_t   *conf;

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mp4 start:%ui, length:%ui", mp4->start, mp4->length);

    conf = ngx_http_get_module_loc_conf(mp4->request, ngx_http_mp4_module);

    mp4->buffer_size = conf->buffer_size;

    rc = ngx_http_mp4_read_atom(mp4, ngx_http_mp4_atoms, mp4->end);
    if (rc != NGX_OK) {
        return rc;
    }

    if (mp4->trak.nelts == 0) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "no mp4 trak atoms were found in \"%s\"",
                      mp4->file.name.data);
        return NGX_ERROR;
    }

    if (mp4->mdat_atom.buf == NULL) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "no mp4 mdat atom was found in \"%s\"",
                      mp4->file.name.data);
        return NGX_ERROR;
    }

    prev = &mp4->out;

    if (mp4->ftyp_atom.buf) {
        *prev = &mp4->ftyp_atom;
        prev = &mp4->ftyp_atom.next;
    }

    *prev = &mp4->moov_atom;
    prev = &mp4->moov_atom.next;

    if (mp4->mvhd_atom.buf) {
        mp4->moov_size += mp4->mvhd_atom_buf.last - mp4->mvhd_atom_buf.pos;
        *prev = &mp4->mvhd_atom;
        prev = &mp4->mvhd_atom.next;
    }

    start_offset = mp4->end;
    end_offset = 0;
    trak = mp4->trak.elts;

    for (i = 0; i < mp4->trak.nelts; i++) {

        if (ngx_http_mp4_update_stts_atom(mp4, &trak[i]) != NGX_OK) {
            return NGX_ERROR;
        }

        if (ngx_http_mp4_update_stss_atom(mp4, &trak[i]) != NGX_OK) {
            return NGX_ERROR;
        }

        ngx_http_mp4_update_ctts_atom(mp4, &trak[i]);

        if (ngx_http_mp4_update_stsc_atom(mp4, &trak[i]) != NGX_OK) {
            return NGX_ERROR;
        }

        if (ngx_http_mp4_update_stsz_atom(mp4, &trak[i]) != NGX_OK) {
            return NGX_ERROR;
        }

        if (trak[i].out[NGX_HTTP_MP4_CO64_DATA].buf) {
            if (ngx_http_mp4_update_co64_atom(mp4, &trak[i]) != NGX_OK) {
                return NGX_ERROR;
            }

        } else {
            if (ngx_http_mp4_update_stco_atom(mp4, &trak[i]) != NGX_OK) {
                return NGX_ERROR;
            }
        }

        ngx_http_mp4_update_stbl_atom(mp4, &trak[i]);
        ngx_http_mp4_update_minf_atom(mp4, &trak[i]);
        ngx_http_mp4_update_mdhd_atom(mp4, &trak[i]);
        trak[i].size += trak[i].hdlr_size;
        ngx_http_mp4_update_mdia_atom(mp4, &trak[i]);
        trak[i].size += trak[i].tkhd_size;
        ngx_http_mp4_update_edts_atom(mp4, &trak[i]);
        ngx_http_mp4_update_trak_atom(mp4, &trak[i]);

        mp4->moov_size += trak[i].size;

        if (start_offset > trak[i].start_offset) {
            start_offset = trak[i].start_offset;
        }

        if (end_offset < trak[i].end_offset) {
            end_offset = trak[i].end_offset;
        }

        *prev = &trak[i].out[NGX_HTTP_MP4_TRAK_ATOM];
        prev = &trak[i].out[NGX_HTTP_MP4_TRAK_ATOM].next;

        for (j = 0; j < NGX_HTTP_MP4_LAST_ATOM + 1; j++) {
            if (trak[i].out[j].buf) {
                *prev = &trak[i].out[j];
                prev = &trak[i].out[j].next;
            }
        }
    }

    if (end_offset < start_offset) {
        end_offset = start_offset;
    }

    mp4->moov_size += 8;

    ngx_mp4_set_32value(mp4->moov_atom_header, mp4->moov_size);
    ngx_mp4_set_atom_name(mp4->moov_atom_header, 'm', 'o', 'o', 'v');
    mp4->content_length += mp4->moov_size;

    *prev = &mp4->mdat_atom;

    if (start_offset > mp4->mdat_data.buf->file_last) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "start time is out mp4 mdat atom in \"%s\"",
                      mp4->file.name.data);
        return NGX_ERROR;
    }

    adjustment = mp4->ftyp_size + mp4->moov_size
                 + ngx_http_mp4_update_mdat_atom(mp4, start_offset, end_offset)
                 - start_offset;

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mp4 adjustment:%O", adjustment);

    for (i = 0; i < mp4->trak.nelts; i++) {
        if (trak[i].out[NGX_HTTP_MP4_CO64_DATA].buf) {
            ngx_http_mp4_adjust_co64_atom(mp4, &trak[i], adjustment);
        } else {
            ngx_http_mp4_adjust_stco_atom(mp4, &trak[i], (int32_t) adjustment);
        }
    }

    return NGX_OK;
}


typedef struct {
    u_char    size[4];
    u_char    name[4];
} ngx_mp4_atom_header_t;

typedef struct {
    u_char    size[4];
    u_char    name[4];
    u_char    size64[8];
} ngx_mp4_atom_header64_t;


static ngx_int_t
ngx_http_mp4_read_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_atom_handler_t *atom, uint64_t atom_data_size)
{
    off_t        end;
    size_t       atom_header_size;
    u_char      *atom_header, *atom_name;
    uint64_t     atom_size;
    ngx_int_t    rc;
    ngx_uint_t   n;

    end = mp4->offset + atom_data_size;

    while (mp4->offset < end) {

        if (ngx_http_mp4_read(mp4, sizeof(uint32_t)) != NGX_OK) {
            return NGX_ERROR;
        }

        atom_header = mp4->buffer_pos;
        atom_size = ngx_mp4_get_32value(atom_header);
        atom_header_size = sizeof(ngx_mp4_atom_header_t);

        if (atom_size == 0) {
            ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                           "mp4 atom end");
            return NGX_OK;
        }

        if (atom_size < sizeof(ngx_mp4_atom_header_t)) {

            if (atom_size == 1) {

                if (ngx_http_mp4_read(mp4, sizeof(ngx_mp4_atom_header64_t))
                    != NGX_OK)
                {
                    return NGX_ERROR;
                }

                /* 64-bit atom size */
                atom_header = mp4->buffer_pos;
                atom_size = ngx_mp4_get_64value(atom_header + 8);
                atom_header_size = sizeof(ngx_mp4_atom_header64_t);

                if (atom_size < sizeof(ngx_mp4_atom_header64_t)) {
                    ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                                  "\"%s\" mp4 atom is too small:%uL",
                                  mp4->file.name.data, atom_size);
                    return NGX_ERROR;
                }

            } else {
                ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                              "\"%s\" mp4 atom is too small:%uL",
                              mp4->file.name.data, atom_size);
                return NGX_ERROR;
            }
        }

        if (ngx_http_mp4_read(mp4, sizeof(ngx_mp4_atom_header_t)) != NGX_OK) {
            return NGX_ERROR;
        }

        atom_header = mp4->buffer_pos;
        atom_name = atom_header + sizeof(uint32_t);

        ngx_log_debug4(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "mp4 atom: %*s @%O:%uL",
                       (size_t) 4, atom_name, mp4->offset, atom_size);

        if (atom_size > (uint64_t) (NGX_MAX_OFF_T_VALUE - mp4->offset)
            || mp4->offset + (off_t) atom_size > end)
        {
            ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                          "\"%s\" mp4 atom too large:%uL",
                          mp4->file.name.data, atom_size);
            return NGX_ERROR;
        }

        for (n = 0; atom[n].name; n++) {

            if (ngx_strncmp(atom_name, atom[n].name, 4) == 0) {

                ngx_mp4_atom_next(mp4, atom_header_size);

                rc = atom[n].handler(mp4, atom_size - atom_header_size);
                if (rc != NGX_OK) {
                    return rc;
                }

                goto next;
            }
        }

        ngx_mp4_atom_next(mp4, atom_size);

    next:
        continue;
    }

    return NGX_OK;
}


static ngx_int_t
ngx_http_mp4_read(ngx_http_mp4_file_t *mp4, size_t size)
{
    ssize_t  n;

    if (mp4->buffer_pos + size <= mp4->buffer_end) {
        return NGX_OK;
    }

    if (mp4->offset + (off_t) mp4->buffer_size > mp4->end) {
        mp4->buffer_size = (size_t) (mp4->end - mp4->offset);
    }

    if (mp4->buffer_size < size) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 file truncated", mp4->file.name.data);
        return NGX_ERROR;
    }

    if (mp4->buffer == NULL) {
        mp4->buffer = ngx_palloc(mp4->request->pool, mp4->buffer_size);
        if (mp4->buffer == NULL) {
            return NGX_ERROR;
        }

        mp4->buffer_start = mp4->buffer;
    }

    n = ngx_read_file(&mp4->file, mp4->buffer_start, mp4->buffer_size,
                      mp4->offset);

    if (n == NGX_ERROR) {
        return NGX_ERROR;
    }

    if ((size_t) n != mp4->buffer_size) {
        ngx_log_error(NGX_LOG_CRIT, mp4->file.log, 0,
                      ngx_read_file_n " read only %z of %z from \"%s\"",
                      n, mp4->buffer_size, mp4->file.name.data);
        return NGX_ERROR;
    }

    mp4->buffer_pos = mp4->buffer_start;
    mp4->buffer_end = mp4->buffer_start + mp4->buffer_size;

    return NGX_OK;
}


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");

    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;
    }

    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;
    }

    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);

    atom = &mp4->ftyp_atom_buf;
    atom->temporary = 1;
    atom->pos = ftyp_atom;
    atom->last = ftyp_atom + atom_size;

    mp4->ftyp_atom.buf = atom;
    mp4->ftyp_size = atom_size;
    mp4->content_length = atom_size;

    ngx_mp4_atom_next(mp4, atom_data_size);

    return NGX_OK;
}


/*
 * 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 = (mp4->mdat_atom.buf == NULL);

    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;
    }

    conf = ngx_http_get_module_loc_conf(mp4->request, ngx_http_mp4_module);

    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;
        }

        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;
    }

    if (ngx_http_mp4_read(mp4, (size_t) atom_data_size) != NGX_OK) {
        return NGX_ERROR;
    }

    mp4->trak.elts = &mp4->traks;
    mp4->trak.size = sizeof(ngx_http_mp4_trak_t);
    mp4->trak.nalloc = 2;
    mp4->trak.pool = mp4->request->pool;

    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;

    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) {
        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 {
        /* skip atoms after moov atom */
        mp4->offset = mp4->end;
    }

    return rc;
}


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");

    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) {
        /* skip atoms after mdat atom */
        mp4->offset = mp4->end;

    } else {
        ngx_mp4_atom_next(mp4, atom_data_size);
    }

    return NGX_OK;
}


static size_t
ngx_http_mp4_update_mdat_atom(ngx_http_mp4_file_t *mp4, off_t start_offset,
    off_t end_offset)
{
    off_t       atom_data_size;
    u_char     *atom_header;
    uint32_t    atom_header_size;
    uint64_t    atom_size;
    ngx_buf_t  *atom;

    atom_data_size = end_offset - start_offset;
    mp4->mdat_data.buf->file_pos = start_offset;
    mp4->mdat_data.buf->file_last = end_offset;

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mdat new offset @%O:%O", start_offset, atom_data_size);

    atom_header = mp4->mdat_atom_header;

    if ((uint64_t) atom_data_size
        > (uint64_t) 0xffffffff - sizeof(ngx_mp4_atom_header_t))
    {
        atom_size = 1;
        atom_header_size = sizeof(ngx_mp4_atom_header64_t);
        ngx_mp4_set_64value(atom_header + sizeof(ngx_mp4_atom_header_t),
                            sizeof(ngx_mp4_atom_header64_t) + atom_data_size);
    } else {
        atom_size = sizeof(ngx_mp4_atom_header_t) + atom_data_size;
        atom_header_size = sizeof(ngx_mp4_atom_header_t);
    }

    mp4->content_length += atom_header_size + atom_data_size;

    ngx_mp4_set_32value(atom_header, atom_size);
    ngx_mp4_set_atom_name(atom_header, 'm', 'd', 'a', 't');

    atom = &mp4->mdat_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_header + atom_header_size;

    return atom_header_size;
}


typedef struct {
    u_char    size[4];
    u_char    name[4];
    u_char    version[1];
    u_char    flags[3];
    u_char    creation_time[4];
    u_char    modification_time[4];
    u_char    timescale[4];
    u_char    duration[4];
    u_char    rate[4];
    u_char    volume[2];
    u_char    reserved[10];
    u_char    matrix[36];
    u_char    preview_time[4];
    u_char    preview_duration[4];
    u_char    poster_time[4];
    u_char    selection_time[4];
    u_char    selection_duration[4];
    u_char    current_time[4];
    u_char    next_track_id[4];
} ngx_mp4_mvhd_atom_t;

typedef struct {
    u_char    size[4];
    u_char    name[4];
    u_char    version[1];
    u_char    flags[3];
    u_char    creation_time[8];
    u_char    modification_time[8];
    u_char    timescale[4];
    u_char    duration[8];
    u_char    rate[4];
    u_char    volume[2];
    u_char    reserved[10];
    u_char    matrix[36];
    u_char    preview_time[4];
    u_char    preview_duration[4];
    u_char    poster_time[4];
    u_char    selection_time[4];
    u_char    selection_duration[4];
    u_char    current_time[4];
    u_char    next_track_id[4];
} ngx_mp4_mvhd64_atom_t;


static ngx_int_t
ngx_http_mp4_read_mvhd_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char                 *atom_header;
    size_t                  atom_size;
    uint32_t                timescale;
    uint64_t                duration, start_time, length_time;
    ngx_buf_t              *atom;
    ngx_mp4_mvhd_atom_t    *mvhd_atom;
    ngx_mp4_mvhd64_atom_t  *mvhd64_atom;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 mvhd atom");

    atom_header = ngx_mp4_atom_header(mp4);
    mvhd_atom = (ngx_mp4_mvhd_atom_t *) atom_header;
    mvhd64_atom = (ngx_mp4_mvhd64_atom_t *) atom_header;
    ngx_mp4_set_atom_name(atom_header, 'm', 'v', 'h', 'd');

    if (ngx_mp4_atom_data_size(ngx_mp4_mvhd_atom_t) > atom_data_size) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 mvhd atom too small", mp4->file.name.data);
        return NGX_ERROR;
    }

    if (mvhd_atom->version[0] == 0) {
        /* version 0: 32-bit duration */
        timescale = ngx_mp4_get_32value(mvhd_atom->timescale);
        duration = ngx_mp4_get_32value(mvhd_atom->duration);

    } else {
        /* version 1: 64-bit duration */

        if (ngx_mp4_atom_data_size(ngx_mp4_mvhd64_atom_t) > atom_data_size) {
            ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                          "\"%s\" mp4 mvhd atom too small",
                          mp4->file.name.data);
            return NGX_ERROR;
        }

        timescale = ngx_mp4_get_32value(mvhd64_atom->timescale);
        duration = ngx_mp4_get_64value(mvhd64_atom->duration);
    }

    mp4->timescale = timescale;

    ngx_log_debug3(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mvhd timescale:%uD, duration:%uL, time:%.3fs",
                   timescale, duration, (double) duration / timescale);

    start_time = (uint64_t) mp4->start * timescale / 1000;

    if (duration < start_time) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 start time exceeds file duration",
                      mp4->file.name.data);
        return NGX_ERROR;
    }

    duration -= start_time;

    if (mp4->length) {
        length_time = (uint64_t) mp4->length * timescale / 1000;

        if (duration > length_time) {
            duration = length_time;
        }
    }

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mvhd new duration:%uL, time:%.3fs",
                   duration, (double) duration / timescale);

    atom_size = sizeof(ngx_mp4_atom_header_t) + (size_t) atom_data_size;
    ngx_mp4_set_32value(mvhd_atom->size, atom_size);

    if (mvhd_atom->version[0] == 0) {
        ngx_mp4_set_32value(mvhd_atom->duration, duration);

    } else {
        ngx_mp4_set_64value(mvhd64_atom->duration, duration);
    }

    atom = &mp4->mvhd_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_header + atom_size;

    mp4->mvhd_atom.buf = atom;

    ngx_mp4_atom_next(mp4, atom_data_size);

    return NGX_OK;
}


static ngx_int_t
ngx_http_mp4_read_trak_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char               *atom_header, *atom_end;
    off_t                 atom_file_end;
    ngx_int_t             rc;
    ngx_buf_t            *atom;
    ngx_http_mp4_trak_t  *trak;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 trak atom");

    trak = ngx_array_push(&mp4->trak);
    if (trak == NULL) {
        return NGX_ERROR;
    }

    ngx_memzero(trak, sizeof(ngx_http_mp4_trak_t));

    atom_header = ngx_mp4_atom_header(mp4);
    ngx_mp4_set_atom_name(atom_header, 't', 'r', 'a', 'k');

    atom = &trak->trak_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_header + sizeof(ngx_mp4_atom_header_t);

    trak->out[NGX_HTTP_MP4_TRAK_ATOM].buf = atom;

    atom_end = mp4->buffer_pos + (size_t) atom_data_size;
    atom_file_end = mp4->offset + atom_data_size;

    rc = ngx_http_mp4_read_atom(mp4, ngx_http_mp4_trak_atoms, atom_data_size);

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mp4 trak atom: %i", rc);

    if (rc == NGX_DECLINED) {
        /* skip this trak */
        ngx_memzero(trak, sizeof(ngx_http_mp4_trak_t));
        mp4->trak.nelts--;
        mp4->buffer_pos = atom_end;
        mp4->offset = atom_file_end;
        return NGX_OK;
    }

    return rc;
}


static void
ngx_http_mp4_update_trak_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak)
{
    ngx_buf_t  *atom;

    trak->size += sizeof(ngx_mp4_atom_header_t);
    atom = &trak->trak_atom_buf;
    ngx_mp4_set_32value(atom->pos, trak->size);
}


static ngx_int_t
ngx_http_mp4_read_cmov_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                  "\"%s\" mp4 compressed moov atom (cmov) is not supported",
                  mp4->file.name.data);

    return NGX_ERROR;
}


typedef struct {
    u_char    size[4];
    u_char    name[4];
    u_char    version[1];
    u_char    flags[3];
    u_char    creation_time[4];
    u_char    modification_time[4];
    u_char    track_id[4];
    u_char    reserved1[4];
    u_char    duration[4];
    u_char    reserved2[8];
    u_char    layer[2];
    u_char    group[2];
    u_char    volume[2];
    u_char    reserved3[2];
    u_char    matrix[36];
    u_char    width[4];
    u_char    height[4];
} ngx_mp4_tkhd_atom_t;

typedef struct {
    u_char    size[4];
    u_char    name[4];
    u_char    version[1];
    u_char    flags[3];
    u_char    creation_time[8];
    u_char    modification_time[8];
    u_char    track_id[4];
    u_char    reserved1[4];
    u_char    duration[8];
    u_char    reserved2[8];
    u_char    layer[2];
    u_char    group[2];
    u_char    volume[2];
    u_char    reserved3[2];
    u_char    matrix[36];
    u_char    width[4];
    u_char    height[4];
} ngx_mp4_tkhd64_atom_t;


static ngx_int_t
ngx_http_mp4_read_tkhd_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char                 *atom_header;
    size_t                  atom_size;
    uint64_t                duration, start_time, length_time;
    ngx_buf_t              *atom;
    ngx_http_mp4_trak_t    *trak;
    ngx_mp4_tkhd_atom_t    *tkhd_atom;
    ngx_mp4_tkhd64_atom_t  *tkhd64_atom;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 tkhd atom");

    atom_header = ngx_mp4_atom_header(mp4);
    tkhd_atom = (ngx_mp4_tkhd_atom_t *) atom_header;
    tkhd64_atom = (ngx_mp4_tkhd64_atom_t *) atom_header;
    ngx_mp4_set_atom_name(tkhd_atom, 't', 'k', 'h', 'd');

    if (ngx_mp4_atom_data_size(ngx_mp4_tkhd_atom_t) > atom_data_size) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 tkhd atom too small", mp4->file.name.data);
        return NGX_ERROR;
    }

    if (tkhd_atom->version[0] == 0) {
        /* version 0: 32-bit duration */
        duration = ngx_mp4_get_32value(tkhd_atom->duration);

    } else {
        /* version 1: 64-bit duration */

        if (ngx_mp4_atom_data_size(ngx_mp4_tkhd64_atom_t) > atom_data_size) {
            ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                          "\"%s\" mp4 tkhd atom too small",
                          mp4->file.name.data);
            return NGX_ERROR;
        }

        duration = ngx_mp4_get_64value(tkhd64_atom->duration);
    }

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "tkhd duration:%uL, time:%.3fs",
                   duration, (double) duration / mp4->timescale);

    start_time = (uint64_t) mp4->start * mp4->timescale / 1000;

    if (duration <= start_time) {
        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "tkhd duration is less than start time");
        return NGX_DECLINED;
    }

    duration -= start_time;

    if (mp4->length) {
        length_time = (uint64_t) mp4->length * mp4->timescale / 1000;

        if (duration > length_time) {
            duration = length_time;
        }
    }

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "tkhd new duration:%uL, time:%.3fs",
                   duration, (double) duration / mp4->timescale);

    atom_size = sizeof(ngx_mp4_atom_header_t) + (size_t) atom_data_size;

    trak = ngx_mp4_last_trak(mp4);
    trak->tkhd_size = atom_size;
    trak->movie_duration = duration;

    ngx_mp4_set_32value(tkhd_atom->size, atom_size);

    if (tkhd_atom->version[0] == 0) {
        ngx_mp4_set_32value(tkhd_atom->duration, duration);

    } else {
        ngx_mp4_set_64value(tkhd64_atom->duration, duration);
    }

    atom = &trak->tkhd_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_header + atom_size;

    trak->out[NGX_HTTP_MP4_TKHD_ATOM].buf = atom;

    ngx_mp4_atom_next(mp4, atom_data_size);

    return NGX_OK;
}


static ngx_int_t
ngx_http_mp4_read_mdia_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char               *atom_header;
    ngx_buf_t            *atom;
    ngx_http_mp4_trak_t  *trak;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "process mdia atom");

    atom_header = ngx_mp4_atom_header(mp4);
    ngx_mp4_set_atom_name(atom_header, 'm', 'd', 'i', 'a');

    trak = ngx_mp4_last_trak(mp4);

    atom = &trak->mdia_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_header + sizeof(ngx_mp4_atom_header_t);

    trak->out[NGX_HTTP_MP4_MDIA_ATOM].buf = atom;

    return ngx_http_mp4_read_atom(mp4, ngx_http_mp4_mdia_atoms, atom_data_size);
}


static void
ngx_http_mp4_update_mdia_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak)
{
    ngx_buf_t  *atom;

    trak->size += sizeof(ngx_mp4_atom_header_t);
    atom = &trak->mdia_atom_buf;
    ngx_mp4_set_32value(atom->pos, trak->size);
}


typedef struct {
    u_char    size[4];
    u_char    name[4];
    u_char    version[1];
    u_char    flags[3];
    u_char    creation_time[4];
    u_char    modification_time[4];
    u_char    timescale[4];
    u_char    duration[4];
    u_char    language[2];
    u_char    quality[2];
} ngx_mp4_mdhd_atom_t;

typedef struct {
    u_char    size[4];
    u_char    name[4];
    u_char    version[1];
    u_char    flags[3];
    u_char    creation_time[8];
    u_char    modification_time[8];
    u_char    timescale[4];
    u_char    duration[8];
    u_char    language[2];
    u_char    quality[2];
} ngx_mp4_mdhd64_atom_t;


static ngx_int_t
ngx_http_mp4_read_mdhd_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char                 *atom_header;
    size_t                  atom_size;
    uint32_t                timescale;
    uint64_t                duration, start_time, length_time;
    ngx_buf_t              *atom;
    ngx_http_mp4_trak_t    *trak;
    ngx_mp4_mdhd_atom_t    *mdhd_atom;
    ngx_mp4_mdhd64_atom_t  *mdhd64_atom;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 mdhd atom");

    atom_header = ngx_mp4_atom_header(mp4);
    mdhd_atom = (ngx_mp4_mdhd_atom_t *) atom_header;
    mdhd64_atom = (ngx_mp4_mdhd64_atom_t *) atom_header;
    ngx_mp4_set_atom_name(mdhd_atom, 'm', 'd', 'h', 'd');

    if (ngx_mp4_atom_data_size(ngx_mp4_mdhd_atom_t) > atom_data_size) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 mdhd atom too small", mp4->file.name.data);
        return NGX_ERROR;
    }

    if (mdhd_atom->version[0] == 0) {
        /* version 0: everything is 32-bit */
        timescale = ngx_mp4_get_32value(mdhd_atom->timescale);
        duration = ngx_mp4_get_32value(mdhd_atom->duration);

    } else {
        /* version 1: 64-bit duration and 32-bit timescale */

        if (ngx_mp4_atom_data_size(ngx_mp4_mdhd64_atom_t) > atom_data_size) {
            ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                          "\"%s\" mp4 mdhd atom too small",
                          mp4->file.name.data);
            return NGX_ERROR;
        }

        timescale = ngx_mp4_get_32value(mdhd64_atom->timescale);
        duration = ngx_mp4_get_64value(mdhd64_atom->duration);
    }

    ngx_log_debug3(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mdhd timescale:%uD, duration:%uL, time:%.3fs",
                   timescale, duration, (double) duration / timescale);

    start_time = (uint64_t) mp4->start * timescale / 1000;

    if (duration <= start_time) {
        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "mdhd duration is less than start time");
        return NGX_DECLINED;
    }

    duration -= start_time;

    if (mp4->length) {
        length_time = (uint64_t) mp4->length * timescale / 1000;

        if (duration > length_time) {
            duration = length_time;
        }
    }

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mdhd new duration:%uL, time:%.3fs",
                   duration, (double) duration / timescale);

    atom_size = sizeof(ngx_mp4_atom_header_t) + (size_t) atom_data_size;

    trak = ngx_mp4_last_trak(mp4);
    trak->mdhd_size = atom_size;
    trak->timescale = timescale;
    trak->duration = duration;

    ngx_mp4_set_32value(mdhd_atom->size, atom_size);

    atom = &trak->mdhd_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_header + atom_size;

    trak->out[NGX_HTTP_MP4_MDHD_ATOM].buf = atom;

    ngx_mp4_atom_next(mp4, atom_data_size);

    return NGX_OK;
}


static void
ngx_http_mp4_update_mdhd_atom(ngx_http_mp4_file_t *mp4,
            ngx_http_mp4_trak_t *trak)
{
    ngx_buf_t              *atom;
    ngx_mp4_mdhd_atom_t    *mdhd_atom;
    ngx_mp4_mdhd64_atom_t  *mdhd64_atom;

    atom = trak->out[NGX_HTTP_MP4_MDHD_ATOM].buf;
    if (atom == NULL) {
        return;
    }

    mdhd_atom = (ngx_mp4_mdhd_atom_t *) atom->pos;
    mdhd64_atom = (ngx_mp4_mdhd64_atom_t *) atom->pos;

    if (mdhd_atom->version[0] == 0) {
        ngx_mp4_set_32value(mdhd_atom->duration, trak->duration);

    } else {
        ngx_mp4_set_64value(mdhd64_atom->duration, trak->duration);
    }

    trak->size += trak->mdhd_size;
}


static ngx_int_t
ngx_http_mp4_read_hdlr_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char              *atom_header;
    size_t               atom_size;
    ngx_buf_t            *atom;
    ngx_http_mp4_trak_t  *trak;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 hdlr atom");

    atom_header = ngx_mp4_atom_header(mp4);
    atom_size = sizeof(ngx_mp4_atom_header_t) + (size_t) atom_data_size;
    ngx_mp4_set_32value(atom_header, atom_size);
    ngx_mp4_set_atom_name(atom_header, 'h', 'd', 'l', 'r');

    trak = ngx_mp4_last_trak(mp4);

    atom = &trak->hdlr_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_header + atom_size;

    trak->hdlr_size = atom_size;
    trak->out[NGX_HTTP_MP4_HDLR_ATOM].buf = atom;

    ngx_mp4_atom_next(mp4, atom_data_size);

    return NGX_OK;
}


static ngx_int_t
ngx_http_mp4_read_minf_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char               *atom_header;
    ngx_buf_t            *atom;
    ngx_http_mp4_trak_t  *trak;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "process minf atom");

    atom_header = ngx_mp4_atom_header(mp4);
    ngx_mp4_set_atom_name(atom_header, 'm', 'i', 'n', 'f');

    trak = ngx_mp4_last_trak(mp4);

    atom = &trak->minf_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_header + sizeof(ngx_mp4_atom_header_t);

    trak->out[NGX_HTTP_MP4_MINF_ATOM].buf = atom;

    return ngx_http_mp4_read_atom(mp4, ngx_http_mp4_minf_atoms, atom_data_size);
}


static void
ngx_http_mp4_update_minf_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak)
{
    ngx_buf_t  *atom;

    trak->size += sizeof(ngx_mp4_atom_header_t)
               + trak->vmhd_size
               + trak->smhd_size
               + trak->dinf_size;
    atom = &trak->minf_atom_buf;
    ngx_mp4_set_32value(atom->pos, trak->size);
}


static ngx_int_t
ngx_http_mp4_read_vmhd_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char              *atom_header;
    size_t               atom_size;
    ngx_buf_t            *atom;
    ngx_http_mp4_trak_t  *trak;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 vmhd atom");

    atom_header = ngx_mp4_atom_header(mp4);
    atom_size = sizeof(ngx_mp4_atom_header_t) + (size_t) atom_data_size;
    ngx_mp4_set_32value(atom_header, atom_size);
    ngx_mp4_set_atom_name(atom_header, 'v', 'm', 'h', 'd');

    trak = ngx_mp4_last_trak(mp4);

    atom = &trak->vmhd_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_header + atom_size;

    trak->vmhd_size += atom_size;
    trak->out[NGX_HTTP_MP4_VMHD_ATOM].buf = atom;

    ngx_mp4_atom_next(mp4, atom_data_size);

    return NGX_OK;
}


static ngx_int_t
ngx_http_mp4_read_smhd_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char              *atom_header;
    size_t               atom_size;
    ngx_buf_t            *atom;
    ngx_http_mp4_trak_t  *trak;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 smhd atom");

    atom_header = ngx_mp4_atom_header(mp4);
    atom_size = sizeof(ngx_mp4_atom_header_t) + (size_t) atom_data_size;
    ngx_mp4_set_32value(atom_header, atom_size);
    ngx_mp4_set_atom_name(atom_header, 's', 'm', 'h', 'd');

    trak = ngx_mp4_last_trak(mp4);

    atom = &trak->smhd_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_header + atom_size;

    trak->smhd_size += atom_size;
    trak->out[NGX_HTTP_MP4_SMHD_ATOM].buf = atom;

    ngx_mp4_atom_next(mp4, atom_data_size);

    return NGX_OK;
}


static ngx_int_t
ngx_http_mp4_read_dinf_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char              *atom_header;
    size_t               atom_size;
    ngx_buf_t            *atom;
    ngx_http_mp4_trak_t  *trak;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 dinf atom");

    atom_header = ngx_mp4_atom_header(mp4);
    atom_size = sizeof(ngx_mp4_atom_header_t) + (size_t) atom_data_size;
    ngx_mp4_set_32value(atom_header, atom_size);
    ngx_mp4_set_atom_name(atom_header, 'd', 'i', 'n', 'f');

    trak = ngx_mp4_last_trak(mp4);

    atom = &trak->dinf_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_header + atom_size;

    trak->dinf_size += atom_size;
    trak->out[NGX_HTTP_MP4_DINF_ATOM].buf = atom;

    ngx_mp4_atom_next(mp4, atom_data_size);

    return NGX_OK;
}


static ngx_int_t
ngx_http_mp4_read_stbl_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char               *atom_header;
    ngx_buf_t            *atom;
    ngx_http_mp4_trak_t  *trak;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "process stbl atom");

    atom_header = ngx_mp4_atom_header(mp4);
    ngx_mp4_set_atom_name(atom_header, 's', 't', 'b', 'l');

    trak = ngx_mp4_last_trak(mp4);

    atom = &trak->stbl_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_header + sizeof(ngx_mp4_atom_header_t);

    trak->out[NGX_HTTP_MP4_STBL_ATOM].buf = atom;

    return ngx_http_mp4_read_atom(mp4, ngx_http_mp4_stbl_atoms, atom_data_size);
}


static void
ngx_http_mp4_update_edts_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak)
{
    ngx_buf_t            *atom;
    ngx_mp4_elst_atom_t  *elst_atom;
    ngx_mp4_edts_atom_t  *edts_atom;

    if (trak->prefix == 0) {
        return;
    }

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mp4 edts atom update prefix:%uL", trak->prefix);

    edts_atom = &trak->edts_atom;
    ngx_mp4_set_32value(edts_atom->size, sizeof(ngx_mp4_edts_atom_t)
                                         + sizeof(ngx_mp4_elst_atom_t));
    ngx_mp4_set_atom_name(edts_atom, 'e', 'd', 't', 's');

    atom = &trak->edts_atom_buf;
    atom->temporary = 1;
    atom->pos = (u_char *) edts_atom;
    atom->last = (u_char *) edts_atom + sizeof(ngx_mp4_edts_atom_t);

    trak->out[NGX_HTTP_MP4_EDTS_ATOM].buf = atom;

    elst_atom = &trak->elst_atom;
    ngx_mp4_set_32value(elst_atom->size, sizeof(ngx_mp4_elst_atom_t));
    ngx_mp4_set_atom_name(elst_atom, 'e', 'l', 's', 't');

    elst_atom->version[0] = 1;
    elst_atom->flags[0] = 0;
    elst_atom->flags[1] = 0;
    elst_atom->flags[2] = 0;

    ngx_mp4_set_32value(elst_atom->entries, 1);
    ngx_mp4_set_64value(elst_atom->duration, trak->movie_duration);
    ngx_mp4_set_64value(elst_atom->media_time, trak->prefix);
    ngx_mp4_set_16value(elst_atom->media_rate, 1);
    ngx_mp4_set_16value(elst_atom->reserved, 0);

    atom = &trak->elst_atom_buf;
    atom->temporary = 1;
    atom->pos = (u_char *) elst_atom;
    atom->last = (u_char *) elst_atom + sizeof(ngx_mp4_elst_atom_t);

    trak->out[NGX_HTTP_MP4_ELST_ATOM].buf = atom;

    trak->size += sizeof(ngx_mp4_edts_atom_t) + sizeof(ngx_mp4_elst_atom_t);
}


static void
ngx_http_mp4_update_stbl_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak)
{
    ngx_buf_t  *atom;

    trak->size += sizeof(ngx_mp4_atom_header_t);
    atom = &trak->stbl_atom_buf;
    ngx_mp4_set_32value(atom->pos, trak->size);
}


typedef struct {
    u_char    size[4];
    u_char    name[4];
    u_char    version[1];
    u_char    flags[3];
    u_char    entries[4];

    u_char    media_size[4];
    u_char    media_name[4];
} ngx_mp4_stsd_atom_t;


static ngx_int_t
ngx_http_mp4_read_stsd_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char               *atom_header, *atom_table;
    size_t                atom_size;
    ngx_buf_t            *atom;
    ngx_mp4_stsd_atom_t  *stsd_atom;
    ngx_http_mp4_trak_t  *trak;

    /* sample description atom */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 stsd atom");

    atom_header = ngx_mp4_atom_header(mp4);
    stsd_atom = (ngx_mp4_stsd_atom_t *) atom_header;
    atom_size = sizeof(ngx_mp4_atom_header_t) + (size_t) atom_data_size;
    atom_table = atom_header + atom_size;
    ngx_mp4_set_32value(stsd_atom->size, atom_size);
    ngx_mp4_set_atom_name(stsd_atom, 's', 't', 's', 'd');

    if (ngx_mp4_atom_data_size(ngx_mp4_stsd_atom_t) > atom_data_size) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 stsd atom too small", mp4->file.name.data);
        return NGX_ERROR;
    }

    ngx_log_debug3(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "stsd entries:%uD, media:%*s",
                   ngx_mp4_get_32value(stsd_atom->entries),
                   (size_t) 4, stsd_atom->media_name);

    trak = ngx_mp4_last_trak(mp4);

    atom = &trak->stsd_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_table;

    trak->out[NGX_HTTP_MP4_STSD_ATOM].buf = atom;
    trak->size += atom_size;

    ngx_mp4_atom_next(mp4, atom_data_size);

    return NGX_OK;
}


typedef struct {
    u_char    size[4];
    u_char    name[4];
    u_char    version[1];
    u_char    flags[3];
    u_char    entries[4];
} ngx_mp4_stts_atom_t;

typedef struct {
    u_char    count[4];
    u_char    duration[4];
} ngx_mp4_stts_entry_t;


static ngx_int_t
ngx_http_mp4_read_stts_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char               *atom_header, *atom_table, *atom_end;
    uint32_t              entries;
    ngx_buf_t            *atom, *data;
    ngx_mp4_stts_atom_t  *stts_atom;
    ngx_http_mp4_trak_t  *trak;

    /* time-to-sample atom */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 stts atom");

    atom_header = ngx_mp4_atom_header(mp4);
    stts_atom = (ngx_mp4_stts_atom_t *) atom_header;
    ngx_mp4_set_atom_name(stts_atom, 's', 't', 't', 's');

    if (ngx_mp4_atom_data_size(ngx_mp4_stts_atom_t) > atom_data_size) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 stts atom too small", mp4->file.name.data);
        return NGX_ERROR;
    }

    entries = ngx_mp4_get_32value(stts_atom->entries);

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mp4 time-to-sample entries:%uD", entries);

    if (ngx_mp4_atom_data_size(ngx_mp4_stts_atom_t)
        + entries * sizeof(ngx_mp4_stts_entry_t) > atom_data_size)
    {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 stts atom too small", mp4->file.name.data);
        return NGX_ERROR;
    }

    atom_table = atom_header + sizeof(ngx_mp4_stts_atom_t);
    atom_end = atom_table + entries * sizeof(ngx_mp4_stts_entry_t);

    trak = ngx_mp4_last_trak(mp4);
    trak->time_to_sample_entries = entries;

    atom = &trak->stts_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_table;

    data = &trak->stts_data_buf;
    data->temporary = 1;
    data->pos = atom_table;
    data->last = atom_end;

    trak->out[NGX_HTTP_MP4_STTS_ATOM].buf = atom;
    trak->out[NGX_HTTP_MP4_STTS_DATA].buf = data;

    ngx_mp4_atom_next(mp4, atom_data_size);

    return NGX_OK;
}


static ngx_int_t
ngx_http_mp4_update_stts_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak)
{
    size_t                atom_size;
    ngx_buf_t            *atom, *data;
    ngx_mp4_stts_atom_t  *stts_atom;

    /*
     * mdia.minf.stbl.stts updating requires trak->timescale
     * from mdia.mdhd atom which may reside after mdia.minf
     */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mp4 stts atom update");

    data = trak->out[NGX_HTTP_MP4_STTS_DATA].buf;

    if (data == NULL) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "no mp4 stts atoms were found in \"%s\"",
                      mp4->file.name.data);
        return NGX_ERROR;
    }

    if (ngx_http_mp4_crop_stts_data(mp4, trak, 1) != NGX_OK) {
        return NGX_ERROR;
    }

    if (ngx_http_mp4_crop_stts_data(mp4, trak, 0) != NGX_OK) {
        return NGX_ERROR;
    }

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "time-to-sample entries:%uD", trak->time_to_sample_entries);

    atom_size = sizeof(ngx_mp4_stts_atom_t) + (data->last - data->pos);
    trak->size += atom_size;

    atom = trak->out[NGX_HTTP_MP4_STTS_ATOM].buf;
    stts_atom = (ngx_mp4_stts_atom_t *) atom->pos;
    ngx_mp4_set_32value(stts_atom->size, atom_size);
    ngx_mp4_set_32value(stts_atom->entries, trak->time_to_sample_entries);

    return NGX_OK;
}


static ngx_int_t
ngx_http_mp4_crop_stts_data(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak, ngx_uint_t start)
{
    uint32_t               count, duration, rest, key_prefix;
    uint64_t               start_time;
    ngx_buf_t             *data;
    ngx_uint_t             start_sample, entries, start_sec;
    ngx_mp4_stts_entry_t  *entry, *end;

    if (start) {
        start_sec = mp4->start;

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "mp4 stts crop start_time:%ui", start_sec);

    } else if (mp4->length) {
        start_sec = mp4->length;

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "mp4 stts crop end_time:%ui", start_sec);

    } else {
        return NGX_OK;
    }

    data = trak->out[NGX_HTTP_MP4_STTS_DATA].buf;

    start_time = (uint64_t) start_sec * trak->timescale / 1000 + trak->prefix;

    entries = trak->time_to_sample_entries;
    start_sample = 0;
    entry = (ngx_mp4_stts_entry_t *) data->pos;
    end = (ngx_mp4_stts_entry_t *) data->last;

    while (entry < end) {
        count = ngx_mp4_get_32value(entry->count);
        duration = ngx_mp4_get_32value(entry->duration);

        ngx_log_debug3(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "time:%uL, count:%uD, duration:%uD",
                       start_time, count, duration);

        if (start_time < (uint64_t) count * duration) {
            start_sample += (ngx_uint_t) (start_time / duration);
            rest = (uint32_t) (start_time / duration);
            goto found;
        }

        start_sample += count;
        start_time -= count * duration;
        entries--;
        entry++;
    }

    if (start) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "start time is out mp4 stts samples in \"%s\"",
                      mp4->file.name.data);

        return NGX_ERROR;

    } else {
        trak->end_sample = trak->start_sample + start_sample;

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "end_sample:%ui", trak->end_sample);

        return NGX_OK;
    }

found:

    if (start) {
        key_prefix = ngx_http_mp4_seek_key_frame(mp4, trak, start_sample);

        start_sample -= key_prefix;

        while (rest < key_prefix) {
            trak->prefix += rest * duration;
            key_prefix -= rest;

            entry--;
            entries++;

            count = ngx_mp4_get_32value(entry->count);
            duration = ngx_mp4_get_32value(entry->duration);
            rest = count;
        }

        trak->prefix += key_prefix * duration;
        trak->duration += trak->prefix;
        rest -= key_prefix;

        ngx_mp4_set_32value(entry->count, count - rest);
        data->pos = (u_char *) entry;
        trak->time_to_sample_entries = entries;
        trak->start_sample = start_sample;

        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "start_sample:%ui, new count:%uD",
                       trak->start_sample, count - rest);

    } else {
        ngx_mp4_set_32value(entry->count, rest);
        data->last = (u_char *) (entry + 1);
        trak->time_to_sample_entries -= entries - 1;
        trak->end_sample = trak->start_sample + start_sample;

        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "end_sample:%ui, new count:%uD",
                       trak->end_sample, rest);
    }

    return NGX_OK;
}


static uint32_t
ngx_http_mp4_seek_key_frame(ngx_http_mp4_file_t *mp4, ngx_http_mp4_trak_t *trak,
    uint32_t start_sample)
{
    uint32_t              key_prefix, sample, *entry, *end;
    ngx_buf_t            *data;
    ngx_http_mp4_conf_t  *conf;

    conf = ngx_http_get_module_loc_conf(mp4->request, ngx_http_mp4_module);
    if (!conf->start_key_frame) {
        return 0;
    }

    data = trak->out[NGX_HTTP_MP4_STSS_DATA].buf;
    if (data == NULL) {
        return 0;
    }

    entry = (uint32_t *) data->pos;
    end = (uint32_t *) data->last;

    /* sync samples starts from 1 */
    start_sample++;

    key_prefix = 0;

    while (entry < end) {
        sample = ngx_mp4_get_32value(entry);
        if (sample > start_sample) {
            break;
        }

        key_prefix = start_sample - sample;
        entry++;
    }

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mp4 key frame prefix:%uD", key_prefix);

    return key_prefix;
}


typedef struct {
    u_char    size[4];
    u_char    name[4];
    u_char    version[1];
    u_char    flags[3];
    u_char    entries[4];
} ngx_http_mp4_stss_atom_t;


static ngx_int_t
ngx_http_mp4_read_stss_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char                    *atom_header, *atom_table, *atom_end;
    uint32_t                   entries;
    ngx_buf_t                 *atom, *data;
    ngx_http_mp4_trak_t       *trak;
    ngx_http_mp4_stss_atom_t  *stss_atom;

    /* sync samples atom */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 stss atom");

    atom_header = ngx_mp4_atom_header(mp4);
    stss_atom = (ngx_http_mp4_stss_atom_t *) atom_header;
    ngx_mp4_set_atom_name(stss_atom, 's', 't', 's', 's');

    if (ngx_mp4_atom_data_size(ngx_http_mp4_stss_atom_t) > atom_data_size) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 stss atom too small", mp4->file.name.data);
        return NGX_ERROR;
    }

    entries = ngx_mp4_get_32value(stss_atom->entries);

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "sync sample entries:%uD", entries);

    trak = ngx_mp4_last_trak(mp4);
    trak->sync_samples_entries = entries;

    atom_table = atom_header + sizeof(ngx_http_mp4_stss_atom_t);

    atom = &trak->stss_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_table;

    if (ngx_mp4_atom_data_size(ngx_http_mp4_stss_atom_t)
        + entries * sizeof(uint32_t) > atom_data_size)
    {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 stss atom too small", mp4->file.name.data);
        return NGX_ERROR;
    }

    atom_end = atom_table + entries * sizeof(uint32_t);

    data = &trak->stss_data_buf;
    data->temporary = 1;
    data->pos = atom_table;
    data->last = atom_end;

    trak->out[NGX_HTTP_MP4_STSS_ATOM].buf = atom;
    trak->out[NGX_HTTP_MP4_STSS_DATA].buf = data;

    ngx_mp4_atom_next(mp4, atom_data_size);

    return NGX_OK;
}


static ngx_int_t
ngx_http_mp4_update_stss_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak)
{
    size_t                     atom_size;
    uint32_t                   sample, start_sample, *entry, *end;
    ngx_buf_t                 *atom, *data;
    ngx_http_mp4_stss_atom_t  *stss_atom;

    /*
     * mdia.minf.stbl.stss updating requires trak->start_sample
     * from mdia.minf.stbl.stts which depends on value from mdia.mdhd
     * atom which may reside after mdia.minf
     */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mp4 stss atom update");

    data = trak->out[NGX_HTTP_MP4_STSS_DATA].buf;

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

    ngx_http_mp4_crop_stss_data(mp4, trak, 1);
    ngx_http_mp4_crop_stss_data(mp4, trak, 0);

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "sync sample entries:%uD", trak->sync_samples_entries);

    if (trak->sync_samples_entries) {
        entry = (uint32_t *) data->pos;
        end = (uint32_t *) data->last;

        start_sample = trak->start_sample;

        while (entry < end) {
            sample = ngx_mp4_get_32value(entry);
            sample -= start_sample;
            ngx_mp4_set_32value(entry, sample);
            entry++;
        }

    } else {
        trak->out[NGX_HTTP_MP4_STSS_DATA].buf = NULL;
    }

    atom_size = sizeof(ngx_http_mp4_stss_atom_t) + (data->last - data->pos);
    trak->size += atom_size;

    atom = trak->out[NGX_HTTP_MP4_STSS_ATOM].buf;
    stss_atom = (ngx_http_mp4_stss_atom_t *) atom->pos;

    ngx_mp4_set_32value(stss_atom->size, atom_size);
    ngx_mp4_set_32value(stss_atom->entries, trak->sync_samples_entries);

    return NGX_OK;
}


static void
ngx_http_mp4_crop_stss_data(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak, ngx_uint_t start)
{
    uint32_t     sample, start_sample, *entry, *end;
    ngx_buf_t   *data;
    ngx_uint_t   entries;

    /* sync samples starts from 1 */

    if (start) {
        start_sample = trak->start_sample + 1;

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "mp4 stss crop start_sample:%uD", start_sample);

    } else if (mp4->length) {
        start_sample = trak->end_sample + 1;

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "mp4 stss crop end_sample:%uD", start_sample);

    } else {
        return;
    }

    data = trak->out[NGX_HTTP_MP4_STSS_DATA].buf;

    entries = trak->sync_samples_entries;
    entry = (uint32_t *) data->pos;
    end = (uint32_t *) data->last;

    while (entry < end) {
        sample = ngx_mp4_get_32value(entry);

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "sync:%uD", sample);

        if (sample >= start_sample) {
            goto found;
        }

        entries--;
        entry++;
    }

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "sample is out of mp4 stss atom");

found:

    if (start) {
        data->pos = (u_char *) entry;
        trak->sync_samples_entries = entries;

    } else {
        data->last = (u_char *) entry;
        trak->sync_samples_entries -= entries;
    }
}


typedef struct {
    u_char    size[4];
    u_char    name[4];
    u_char    version[1];
    u_char    flags[3];
    u_char    entries[4];
} ngx_mp4_ctts_atom_t;

typedef struct {
    u_char    count[4];
    u_char    offset[4];
} ngx_mp4_ctts_entry_t;


static ngx_int_t
ngx_http_mp4_read_ctts_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char               *atom_header, *atom_table, *atom_end;
    uint32_t              entries;
    ngx_buf_t            *atom, *data;
    ngx_mp4_ctts_atom_t  *ctts_atom;
    ngx_http_mp4_trak_t  *trak;

    /* composition offsets atom */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 ctts atom");

    atom_header = ngx_mp4_atom_header(mp4);
    ctts_atom = (ngx_mp4_ctts_atom_t *) atom_header;
    ngx_mp4_set_atom_name(ctts_atom, 'c', 't', 't', 's');

    if (ngx_mp4_atom_data_size(ngx_mp4_ctts_atom_t) > atom_data_size) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 ctts atom too small", mp4->file.name.data);
        return NGX_ERROR;
    }

    entries = ngx_mp4_get_32value(ctts_atom->entries);

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "composition offset entries:%uD", entries);

    trak = ngx_mp4_last_trak(mp4);
    trak->composition_offset_entries = entries;

    atom_table = atom_header + sizeof(ngx_mp4_ctts_atom_t);

    atom = &trak->ctts_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_table;

    if (ngx_mp4_atom_data_size(ngx_mp4_ctts_atom_t)
        + entries * sizeof(ngx_mp4_ctts_entry_t) > atom_data_size)
    {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 ctts atom too small", mp4->file.name.data);
        return NGX_ERROR;
    }

    atom_end = atom_table + entries * sizeof(ngx_mp4_ctts_entry_t);

    data = &trak->ctts_data_buf;
    data->temporary = 1;
    data->pos = atom_table;
    data->last = atom_end;

    trak->out[NGX_HTTP_MP4_CTTS_ATOM].buf = atom;
    trak->out[NGX_HTTP_MP4_CTTS_DATA].buf = data;

    ngx_mp4_atom_next(mp4, atom_data_size);

    return NGX_OK;
}


static void
ngx_http_mp4_update_ctts_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak)
{
    size_t                atom_size;
    ngx_buf_t            *atom, *data;
    ngx_mp4_ctts_atom_t  *ctts_atom;

    /*
     * mdia.minf.stbl.ctts updating requires trak->start_sample
     * from mdia.minf.stbl.stts which depends on value from mdia.mdhd
     * atom which may reside after mdia.minf
     */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mp4 ctts atom update");

    data = trak->out[NGX_HTTP_MP4_CTTS_DATA].buf;

    if (data == NULL) {
        return;
    }

    ngx_http_mp4_crop_ctts_data(mp4, trak, 1);
    ngx_http_mp4_crop_ctts_data(mp4, trak, 0);

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "composition offset entries:%uD",
                   trak->composition_offset_entries);

    if (trak->composition_offset_entries == 0) {
        trak->out[NGX_HTTP_MP4_CTTS_ATOM].buf = NULL;
        trak->out[NGX_HTTP_MP4_CTTS_DATA].buf = NULL;
        return;
    }

    atom_size = sizeof(ngx_mp4_ctts_atom_t) + (data->last - data->pos);
    trak->size += atom_size;

    atom = trak->out[NGX_HTTP_MP4_CTTS_ATOM].buf;
    ctts_atom = (ngx_mp4_ctts_atom_t *) atom->pos;

    ngx_mp4_set_32value(ctts_atom->size, atom_size);
    ngx_mp4_set_32value(ctts_atom->entries, trak->composition_offset_entries);

    return;
}


static void
ngx_http_mp4_crop_ctts_data(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak, ngx_uint_t start)
{
    uint32_t               count, start_sample, rest;
    ngx_buf_t             *data;
    ngx_uint_t             entries;
    ngx_mp4_ctts_entry_t  *entry, *end;

    /* sync samples starts from 1 */

    if (start) {
        start_sample = trak->start_sample + 1;

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "mp4 ctts crop start_sample:%uD", start_sample);

    } else if (mp4->length) {
        start_sample = trak->end_sample - trak->start_sample + 1;

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "mp4 ctts crop end_sample:%uD", start_sample);

    } else {
        return;
    }

    data = trak->out[NGX_HTTP_MP4_CTTS_DATA].buf;

    entries = trak->composition_offset_entries;
    entry = (ngx_mp4_ctts_entry_t *) data->pos;
    end = (ngx_mp4_ctts_entry_t *) data->last;

    while (entry < end) {
        count = ngx_mp4_get_32value(entry->count);

        ngx_log_debug3(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "sample:%uD, count:%uD, offset:%uD",
                       start_sample, count, ngx_mp4_get_32value(entry->offset));

        if (start_sample <= count) {
            rest = start_sample - 1;
            goto found;
        }

        start_sample -= count;
        entries--;
        entry++;
    }

    if (start) {
        data->pos = (u_char *) end;
        trak->composition_offset_entries = 0;
    }

    return;

found:

    if (start) {
        ngx_mp4_set_32value(entry->count, count - rest);
        data->pos = (u_char *) entry;
        trak->composition_offset_entries = entries;

    } else {
        ngx_mp4_set_32value(entry->count, rest);
        data->last = (u_char *) (entry + 1);
        trak->composition_offset_entries -= entries - 1;
    }
}


typedef struct {
    u_char    size[4];
    u_char    name[4];
    u_char    version[1];
    u_char    flags[3];
    u_char    entries[4];
} ngx_mp4_stsc_atom_t;


static ngx_int_t
ngx_http_mp4_read_stsc_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char               *atom_header, *atom_table, *atom_end;
    uint32_t              entries;
    ngx_buf_t            *atom, *data;
    ngx_mp4_stsc_atom_t  *stsc_atom;
    ngx_http_mp4_trak_t  *trak;

    /* sample-to-chunk atom */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 stsc atom");

    atom_header = ngx_mp4_atom_header(mp4);
    stsc_atom = (ngx_mp4_stsc_atom_t *) atom_header;
    ngx_mp4_set_atom_name(stsc_atom, 's', 't', 's', 'c');

    if (ngx_mp4_atom_data_size(ngx_mp4_stsc_atom_t) > atom_data_size) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 stsc atom too small", mp4->file.name.data);
        return NGX_ERROR;
    }

    entries = ngx_mp4_get_32value(stsc_atom->entries);

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "sample-to-chunk entries:%uD", entries);

    if (ngx_mp4_atom_data_size(ngx_mp4_stsc_atom_t)
        + entries * sizeof(ngx_mp4_stsc_entry_t) > atom_data_size)
    {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 stsc atom too small", mp4->file.name.data);
        return NGX_ERROR;
    }

    atom_table = atom_header + sizeof(ngx_mp4_stsc_atom_t);
    atom_end = atom_table + entries * sizeof(ngx_mp4_stsc_entry_t);

    trak = ngx_mp4_last_trak(mp4);
    trak->sample_to_chunk_entries = entries;

    atom = &trak->stsc_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_table;

    data = &trak->stsc_data_buf;
    data->temporary = 1;
    data->pos = atom_table;
    data->last = atom_end;

    trak->out[NGX_HTTP_MP4_STSC_ATOM].buf = atom;
    trak->out[NGX_HTTP_MP4_STSC_DATA].buf = data;

    ngx_mp4_atom_next(mp4, atom_data_size);

    return NGX_OK;
}


static ngx_int_t
ngx_http_mp4_update_stsc_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak)
{
    size_t                 atom_size;
    uint32_t               chunk;
    ngx_buf_t             *atom, *data;
    ngx_mp4_stsc_atom_t   *stsc_atom;
    ngx_mp4_stsc_entry_t  *entry, *end;

    /*
     * mdia.minf.stbl.stsc updating requires trak->start_sample
     * from mdia.minf.stbl.stts which depends on value from mdia.mdhd
     * atom which may reside after mdia.minf
     */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mp4 stsc atom update");

    data = trak->out[NGX_HTTP_MP4_STSC_DATA].buf;

    if (data == NULL) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "no mp4 stsc atoms were found in \"%s\"",
                      mp4->file.name.data);
        return NGX_ERROR;
    }

    if (trak->sample_to_chunk_entries == 0) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "zero number of entries in stsc atom in \"%s\"",
                      mp4->file.name.data);
        return NGX_ERROR;
    }

    if (ngx_http_mp4_crop_stsc_data(mp4, trak, 1) != NGX_OK) {
        return NGX_ERROR;
    }

    if (ngx_http_mp4_crop_stsc_data(mp4, trak, 0) != NGX_OK) {
        return NGX_ERROR;
    }

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "sample-to-chunk entries:%uD",
                   trak->sample_to_chunk_entries);

    entry = (ngx_mp4_stsc_entry_t *) data->pos;
    end = (ngx_mp4_stsc_entry_t *) data->last;

    while (entry < end) {
        chunk = ngx_mp4_get_32value(entry->chunk);
        chunk -= trak->start_chunk;
        ngx_mp4_set_32value(entry->chunk, chunk);
        entry++;
    }

    atom_size = sizeof(ngx_mp4_stsc_atom_t)
                + trak->sample_to_chunk_entries * sizeof(ngx_mp4_stsc_entry_t);

    trak->size += atom_size;

    atom = trak->out[NGX_HTTP_MP4_STSC_ATOM].buf;
    stsc_atom = (ngx_mp4_stsc_atom_t *) atom->pos;

    ngx_mp4_set_32value(stsc_atom->size, atom_size);
    ngx_mp4_set_32value(stsc_atom->entries, trak->sample_to_chunk_entries);

    return NGX_OK;
}


static ngx_int_t
ngx_http_mp4_crop_stsc_data(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak, ngx_uint_t start)
{
    uint32_t               start_sample, chunk, samples, id, next_chunk, n,
                           prev_samples;
    ngx_buf_t             *data, *buf;
    ngx_uint_t             entries, target_chunk, chunk_samples;
    ngx_mp4_stsc_entry_t  *entry, *end, *first;

    entries = trak->sample_to_chunk_entries - 1;

    if (start) {
        start_sample = (uint32_t) trak->start_sample;

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "mp4 stsc crop start_sample:%uD", start_sample);

    } else if (mp4->length) {
        start_sample = (uint32_t) (trak->end_sample - trak->start_sample);
        samples = 0;

        data = trak->out[NGX_HTTP_MP4_STSC_START].buf;

        if (data) {
            entry = (ngx_mp4_stsc_entry_t *) data->pos;
            samples = ngx_mp4_get_32value(entry->samples);
            entries--;

            if (samples > start_sample) {
                samples = start_sample;
                ngx_mp4_set_32value(entry->samples, samples);
            }

            start_sample -= samples;
        }

        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "mp4 stsc crop end_sample:%uD, ext_samples:%uD",
                       start_sample, samples);

    } else {
        return NGX_OK;
    }

    data = trak->out[NGX_HTTP_MP4_STSC_DATA].buf;

    entry = (ngx_mp4_stsc_entry_t *) data->pos;
    end = (ngx_mp4_stsc_entry_t *) data->last;

    chunk = ngx_mp4_get_32value(entry->chunk);
    samples = ngx_mp4_get_32value(entry->samples);
    id = ngx_mp4_get_32value(entry->id);
    prev_samples = 0;
    entry++;

    while (entry < end) {

        next_chunk = ngx_mp4_get_32value(entry->chunk);

        ngx_log_debug5(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "sample:%uD, chunk:%uD, chunks:%uD, "
                       "samples:%uD, id:%uD",
                       start_sample, chunk, next_chunk - chunk, samples, id);

        n = (next_chunk - chunk) * samples;

        if (start_sample < n) {
            goto found;
        }

        start_sample -= n;

        prev_samples = samples;
        chunk = next_chunk;
        samples = ngx_mp4_get_32value(entry->samples);
        id = ngx_mp4_get_32value(entry->id);
        entries--;
        entry++;
    }

    next_chunk = trak->chunks + 1;

    ngx_log_debug4(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "sample:%uD, chunk:%uD, chunks:%uD, samples:%uD",
                   start_sample, chunk, next_chunk - chunk, samples);

    n = (next_chunk - chunk) * samples;

    if (start_sample > n) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "%s time is out mp4 stsc chunks in \"%s\"",
                      start ? "start" : "end", mp4->file.name.data);
        return NGX_ERROR;
    }

found:

    entries++;
    entry--;

    if (samples == 0) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "zero number of samples in \"%s\"",
                      mp4->file.name.data);
        return NGX_ERROR;
    }

    target_chunk = chunk - 1;
    target_chunk += start_sample / samples;
    chunk_samples = start_sample % samples;

    if (start) {
        data->pos = (u_char *) entry;

        trak->sample_to_chunk_entries = entries;
        trak->start_chunk = target_chunk;
        trak->start_chunk_samples = chunk_samples;

        ngx_mp4_set_32value(entry->chunk, trak->start_chunk + 1);

        samples -= chunk_samples;

        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "start_chunk:%ui, start_chunk_samples:%ui",
                       trak->start_chunk, trak->start_chunk_samples);

    } else {
        if (start_sample) {
            data->last = (u_char *) (entry + 1);
            trak->sample_to_chunk_entries -= entries - 1;
            trak->end_chunk_samples = samples;

        } else {
            data->last = (u_char *) entry;
            trak->sample_to_chunk_entries -= entries;
            trak->end_chunk_samples = prev_samples;
        }

        if (chunk_samples) {
            trak->end_chunk = target_chunk + 1;
            trak->end_chunk_samples = chunk_samples;

        } else {
            trak->end_chunk = target_chunk;
        }

        samples = chunk_samples;
        next_chunk = chunk + 1;

        ngx_log_debug2(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "end_chunk:%ui, end_chunk_samples:%ui",
                       trak->end_chunk, trak->end_chunk_samples);
    }

    if (chunk_samples && next_chunk - target_chunk == 2) {

        ngx_mp4_set_32value(entry->samples, samples);

    } else if (chunk_samples && start) {

        first = &trak->stsc_start_chunk_entry;
        ngx_mp4_set_32value(first->chunk, 1);
        ngx_mp4_set_32value(first->samples, samples);
        ngx_mp4_set_32value(first->id, id);

        buf = &trak->stsc_start_chunk_buf;
        buf->temporary = 1;
        buf->pos = (u_char *) first;
        buf->last = (u_char *) first + sizeof(ngx_mp4_stsc_entry_t);

        trak->out[NGX_HTTP_MP4_STSC_START].buf = buf;

        ngx_mp4_set_32value(entry->chunk, trak->start_chunk + 2);

        trak->sample_to_chunk_entries++;

    } else if (chunk_samples) {

        first = &trak->stsc_end_chunk_entry;
        ngx_mp4_set_32value(first->chunk, trak->end_chunk - trak->start_chunk);
        ngx_mp4_set_32value(first->samples, samples);
        ngx_mp4_set_32value(first->id, id);

        buf = &trak->stsc_end_chunk_buf;
        buf->temporary = 1;
        buf->pos = (u_char *) first;
        buf->last = (u_char *) first + sizeof(ngx_mp4_stsc_entry_t);

        trak->out[NGX_HTTP_MP4_STSC_END].buf = buf;

        trak->sample_to_chunk_entries++;
    }

    return NGX_OK;
}


typedef struct {
    u_char    size[4];
    u_char    name[4];
    u_char    version[1];
    u_char    flags[3];
    u_char    uniform_size[4];
    u_char    entries[4];
} ngx_mp4_stsz_atom_t;


static ngx_int_t
ngx_http_mp4_read_stsz_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char               *atom_header, *atom_table, *atom_end;
    size_t                atom_size;
    uint32_t              entries, size;
    ngx_buf_t            *atom, *data;
    ngx_mp4_stsz_atom_t  *stsz_atom;
    ngx_http_mp4_trak_t  *trak;

    /* sample sizes atom */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 stsz atom");

    atom_header = ngx_mp4_atom_header(mp4);
    stsz_atom = (ngx_mp4_stsz_atom_t *) atom_header;
    ngx_mp4_set_atom_name(stsz_atom, 's', 't', 's', 'z');

    if (ngx_mp4_atom_data_size(ngx_mp4_stsz_atom_t) > atom_data_size) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 stsz atom too small", mp4->file.name.data);
        return NGX_ERROR;
    }

    size = ngx_mp4_get_32value(stsz_atom->uniform_size);
    entries = ngx_mp4_get_32value(stsz_atom->entries);

    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "sample uniform size:%uD, entries:%uD", size, entries);

    trak = ngx_mp4_last_trak(mp4);
    trak->sample_sizes_entries = entries;

    atom_table = atom_header + sizeof(ngx_mp4_stsz_atom_t);

    atom = &trak->stsz_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_table;

    trak->out[NGX_HTTP_MP4_STSZ_ATOM].buf = atom;

    if (size == 0) {
        if (ngx_mp4_atom_data_size(ngx_mp4_stsz_atom_t)
            + entries * sizeof(uint32_t) > atom_data_size)
        {
            ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                          "\"%s\" mp4 stsz atom too small",
                          mp4->file.name.data);
            return NGX_ERROR;
        }

        atom_end = atom_table + entries * sizeof(uint32_t);

        data = &trak->stsz_data_buf;
        data->temporary = 1;
        data->pos = atom_table;
        data->last = atom_end;

        trak->out[NGX_HTTP_MP4_STSZ_DATA].buf = data;

    } else {
        /* if size != 0 then all samples are the same size */
        /* TODO : chunk samples */
        atom_size = sizeof(ngx_mp4_atom_header_t) + (size_t) atom_data_size;
        ngx_mp4_set_32value(atom_header, atom_size);
        trak->size += atom_size;
    }

    ngx_mp4_atom_next(mp4, atom_data_size);

    return NGX_OK;
}


static ngx_int_t
ngx_http_mp4_update_stsz_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak)
{
    size_t                atom_size;
    uint32_t             *pos, *end, entries;
    ngx_buf_t            *atom, *data;
    ngx_mp4_stsz_atom_t  *stsz_atom;

    /*
     * mdia.minf.stbl.stsz updating requires trak->start_sample
     * from mdia.minf.stbl.stts which depends on value from mdia.mdhd
     * atom which may reside after mdia.minf
     */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mp4 stsz atom update");

    data = trak->out[NGX_HTTP_MP4_STSZ_DATA].buf;

    if (data) {
        entries = trak->sample_sizes_entries;

        if (trak->start_sample > entries) {
            ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                          "start time is out mp4 stsz samples in \"%s\"",
                          mp4->file.name.data);
            return NGX_ERROR;
        }

        entries -= trak->start_sample;
        data->pos += trak->start_sample * sizeof(uint32_t);
        end = (uint32_t *) data->pos;

        for (pos = end - trak->start_chunk_samples; pos < end; pos++) {
            trak->start_chunk_samples_size += ngx_mp4_get_32value(pos);
        }

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                       "chunk samples sizes:%uL",
                       trak->start_chunk_samples_size);

        if (trak->start_chunk_samples_size > (uint64_t) mp4->end) {
            ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                          "too large mp4 start samples size in \"%s\"",
                          mp4->file.name.data);
            return NGX_ERROR;
        }

        if (mp4->length) {
            if (trak->end_sample - trak->start_sample > entries) {
                ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                              "end time is out mp4 stsz samples in \"%s\"",
                              mp4->file.name.data);
                return NGX_ERROR;
            }

            entries = trak->end_sample - trak->start_sample;
            data->last = data->pos + entries * sizeof(uint32_t);
            end = (uint32_t *) data->last;

            for (pos = end - trak->end_chunk_samples; pos < end; pos++) {
                trak->end_chunk_samples_size += ngx_mp4_get_32value(pos);
            }

            ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                           "mp4 stsz end_chunk_samples_size:%uL",
                           trak->end_chunk_samples_size);

            if (trak->end_chunk_samples_size > (uint64_t) mp4->end) {
                ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                              "too large mp4 end samples size in \"%s\"",
                              mp4->file.name.data);
                return NGX_ERROR;
            }
        }

        atom_size = sizeof(ngx_mp4_stsz_atom_t) + (data->last - data->pos);
        trak->size += atom_size;

        atom = trak->out[NGX_HTTP_MP4_STSZ_ATOM].buf;
        stsz_atom = (ngx_mp4_stsz_atom_t *) atom->pos;

        ngx_mp4_set_32value(stsz_atom->size, atom_size);
        ngx_mp4_set_32value(stsz_atom->entries, entries);
    }

    return NGX_OK;
}


typedef struct {
    u_char    size[4];
    u_char    name[4];
    u_char    version[1];
    u_char    flags[3];
    u_char    entries[4];
} ngx_mp4_stco_atom_t;


static ngx_int_t
ngx_http_mp4_read_stco_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char               *atom_header, *atom_table, *atom_end;
    uint32_t              entries;
    ngx_buf_t            *atom, *data;
    ngx_mp4_stco_atom_t  *stco_atom;
    ngx_http_mp4_trak_t  *trak;

    /* chunk offsets atom */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 stco atom");

    atom_header = ngx_mp4_atom_header(mp4);
    stco_atom = (ngx_mp4_stco_atom_t *) atom_header;
    ngx_mp4_set_atom_name(stco_atom, 's', 't', 'c', 'o');

    if (ngx_mp4_atom_data_size(ngx_mp4_stco_atom_t) > atom_data_size) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 stco atom too small", mp4->file.name.data);
        return NGX_ERROR;
    }

    entries = ngx_mp4_get_32value(stco_atom->entries);

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "chunks:%uD", entries);

    if (ngx_mp4_atom_data_size(ngx_mp4_stco_atom_t)
        + entries * sizeof(uint32_t) > atom_data_size)
    {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 stco atom too small", mp4->file.name.data);
        return NGX_ERROR;
    }

    atom_table = atom_header + sizeof(ngx_mp4_stco_atom_t);
    atom_end = atom_table + entries * sizeof(uint32_t);

    trak = ngx_mp4_last_trak(mp4);
    trak->chunks = entries;

    atom = &trak->stco_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_table;

    data = &trak->stco_data_buf;
    data->temporary = 1;
    data->pos = atom_table;
    data->last = atom_end;

    trak->out[NGX_HTTP_MP4_STCO_ATOM].buf = atom;
    trak->out[NGX_HTTP_MP4_STCO_DATA].buf = data;

    ngx_mp4_atom_next(mp4, atom_data_size);

    return NGX_OK;
}


static ngx_int_t
ngx_http_mp4_update_stco_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak)
{
    size_t                atom_size;
    uint32_t              entries;
    uint64_t              chunk_offset, samples_size;
    ngx_buf_t            *atom, *data;
    ngx_mp4_stco_atom_t  *stco_atom;

    /*
     * mdia.minf.stbl.stco updating requires trak->start_chunk
     * from mdia.minf.stbl.stsc which depends on value from mdia.mdhd
     * atom which may reside after mdia.minf
     */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mp4 stco atom update");

    data = trak->out[NGX_HTTP_MP4_STCO_DATA].buf;

    if (data == NULL) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "no mp4 stco atoms were found in \"%s\"",
                      mp4->file.name.data);
        return NGX_ERROR;
    }

    if (trak->start_chunk > trak->chunks) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "start time is out mp4 stco chunks in \"%s\"",
                      mp4->file.name.data);
        return NGX_ERROR;
    }

    data->pos += trak->start_chunk * sizeof(uint32_t);

    chunk_offset = ngx_mp4_get_32value(data->pos);
    samples_size = trak->start_chunk_samples_size;

    if (chunk_offset > (uint64_t) mp4->end - samples_size
        || chunk_offset + samples_size > NGX_MAX_UINT32_VALUE)
    {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "too large chunk offset in \"%s\"",
                      mp4->file.name.data);
        return NGX_ERROR;
    }

    trak->start_offset = chunk_offset + samples_size;
    ngx_mp4_set_32value(data->pos, trak->start_offset);

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "start chunk offset:%O", trak->start_offset);

    if (mp4->length) {

        if (trak->end_chunk > trak->chunks) {
            ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                          "end time is out mp4 stco chunks in \"%s\"",
                          mp4->file.name.data);
            return NGX_ERROR;
        }

        entries = trak->end_chunk - trak->start_chunk;
        data->last = data->pos + entries * sizeof(uint32_t);

        if (entries) {
            chunk_offset = ngx_mp4_get_32value(data->last - sizeof(uint32_t));
            samples_size = trak->end_chunk_samples_size;

            if (chunk_offset > (uint64_t) mp4->end - samples_size
                || chunk_offset + samples_size > NGX_MAX_UINT32_VALUE)
            {
                ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                              "too large chunk offset in \"%s\"",
                              mp4->file.name.data);
                return NGX_ERROR;
            }

            trak->end_offset = chunk_offset + samples_size;

            ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                           "end chunk offset:%O", trak->end_offset);
        }

    } else {
        entries = trak->chunks - trak->start_chunk;
        trak->end_offset = mp4->mdat_data.buf->file_last;
    }

    if (entries == 0) {
        trak->start_offset = mp4->end;
        trak->end_offset = 0;
    }

    atom_size = sizeof(ngx_mp4_stco_atom_t) + (data->last - data->pos);
    trak->size += atom_size;

    atom = trak->out[NGX_HTTP_MP4_STCO_ATOM].buf;
    stco_atom = (ngx_mp4_stco_atom_t *) atom->pos;

    ngx_mp4_set_32value(stco_atom->size, atom_size);
    ngx_mp4_set_32value(stco_atom->entries, entries);

    return NGX_OK;
}


static void
ngx_http_mp4_adjust_stco_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak, int32_t adjustment)
{
    uint32_t    offset, *entry, *end;
    ngx_buf_t  *data;

    /*
     * moov.trak.mdia.minf.stbl.stco adjustment requires
     * minimal start offset of all traks and new moov atom size
     */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mp4 stco atom adjustment");

    data = trak->out[NGX_HTTP_MP4_STCO_DATA].buf;
    entry = (uint32_t *) data->pos;
    end = (uint32_t *) data->last;

    while (entry < end) {
        offset = ngx_mp4_get_32value(entry);
        offset += adjustment;
        ngx_mp4_set_32value(entry, offset);
        entry++;
    }
}


typedef struct {
    u_char    size[4];
    u_char    name[4];
    u_char    version[1];
    u_char    flags[3];
    u_char    entries[4];
} ngx_mp4_co64_atom_t;


static ngx_int_t
ngx_http_mp4_read_co64_atom(ngx_http_mp4_file_t *mp4, uint64_t atom_data_size)
{
    u_char               *atom_header, *atom_table, *atom_end;
    uint32_t              entries;
    ngx_buf_t            *atom, *data;
    ngx_mp4_co64_atom_t  *co64_atom;
    ngx_http_mp4_trak_t  *trak;

    /* chunk offsets atom */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 co64 atom");

    atom_header = ngx_mp4_atom_header(mp4);
    co64_atom = (ngx_mp4_co64_atom_t *) atom_header;
    ngx_mp4_set_atom_name(co64_atom, 'c', 'o', '6', '4');

    if (ngx_mp4_atom_data_size(ngx_mp4_co64_atom_t) > atom_data_size) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 co64 atom too small", mp4->file.name.data);
        return NGX_ERROR;
    }

    entries = ngx_mp4_get_32value(co64_atom->entries);

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "chunks:%uD", entries);

    if (ngx_mp4_atom_data_size(ngx_mp4_co64_atom_t)
        + entries * sizeof(uint64_t) > atom_data_size)
    {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "\"%s\" mp4 co64 atom too small", mp4->file.name.data);
        return NGX_ERROR;
    }

    atom_table = atom_header + sizeof(ngx_mp4_co64_atom_t);
    atom_end = atom_table + entries * sizeof(uint64_t);

    trak = ngx_mp4_last_trak(mp4);
    trak->chunks = entries;

    atom = &trak->co64_atom_buf;
    atom->temporary = 1;
    atom->pos = atom_header;
    atom->last = atom_table;

    data = &trak->co64_data_buf;
    data->temporary = 1;
    data->pos = atom_table;
    data->last = atom_end;

    trak->out[NGX_HTTP_MP4_CO64_ATOM].buf = atom;
    trak->out[NGX_HTTP_MP4_CO64_DATA].buf = data;

    ngx_mp4_atom_next(mp4, atom_data_size);

    return NGX_OK;
}


static ngx_int_t
ngx_http_mp4_update_co64_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak)
{
    size_t                atom_size;
    uint64_t              entries, chunk_offset, samples_size;
    ngx_buf_t            *atom, *data;
    ngx_mp4_co64_atom_t  *co64_atom;

    /*
     * mdia.minf.stbl.co64 updating requires trak->start_chunk
     * from mdia.minf.stbl.stsc which depends on value from mdia.mdhd
     * atom which may reside after mdia.minf
     */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mp4 co64 atom update");

    data = trak->out[NGX_HTTP_MP4_CO64_DATA].buf;

    if (data == NULL) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "no mp4 co64 atoms were found in \"%s\"",
                      mp4->file.name.data);
        return NGX_ERROR;
    }

    if (trak->start_chunk > trak->chunks) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "start time is out mp4 co64 chunks in \"%s\"",
                      mp4->file.name.data);
        return NGX_ERROR;
    }

    data->pos += trak->start_chunk * sizeof(uint64_t);

    chunk_offset = ngx_mp4_get_64value(data->pos);
    samples_size = trak->start_chunk_samples_size;

    if (chunk_offset > (uint64_t) mp4->end - samples_size) {
        ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                      "too large chunk offset in \"%s\"",
                      mp4->file.name.data);
        return NGX_ERROR;
    }

    trak->start_offset = chunk_offset + samples_size;
    ngx_mp4_set_64value(data->pos, trak->start_offset);

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "start chunk offset:%O", trak->start_offset);

    if (mp4->length) {

        if (trak->end_chunk > trak->chunks) {
            ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                          "end time is out mp4 co64 chunks in \"%s\"",
                          mp4->file.name.data);
            return NGX_ERROR;
        }

        entries = trak->end_chunk - trak->start_chunk;
        data->last = data->pos + entries * sizeof(uint64_t);

        if (entries) {
            chunk_offset = ngx_mp4_get_64value(data->last - sizeof(uint64_t));
            samples_size = trak->end_chunk_samples_size;

            if (chunk_offset > (uint64_t) mp4->end - samples_size) {
                ngx_log_error(NGX_LOG_ERR, mp4->file.log, 0,
                              "too large chunk offset in \"%s\"",
                              mp4->file.name.data);
                return NGX_ERROR;
            }

            trak->end_offset = chunk_offset + samples_size;

            ngx_log_debug1(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                           "end chunk offset:%O", trak->end_offset);
        }

    } else {
        entries = trak->chunks - trak->start_chunk;
        trak->end_offset = mp4->mdat_data.buf->file_last;
    }

    if (entries == 0) {
        trak->start_offset = mp4->end;
        trak->end_offset = 0;
    }

    atom_size = sizeof(ngx_mp4_co64_atom_t) + (data->last - data->pos);
    trak->size += atom_size;

    atom = trak->out[NGX_HTTP_MP4_CO64_ATOM].buf;
    co64_atom = (ngx_mp4_co64_atom_t *) atom->pos;

    ngx_mp4_set_32value(co64_atom->size, atom_size);
    ngx_mp4_set_32value(co64_atom->entries, entries);

    return NGX_OK;
}


static void
ngx_http_mp4_adjust_co64_atom(ngx_http_mp4_file_t *mp4,
    ngx_http_mp4_trak_t *trak, off_t adjustment)
{
    uint64_t    offset, *entry, *end;
    ngx_buf_t  *data;

    /*
     * moov.trak.mdia.minf.stbl.co64 adjustment requires
     * minimal start offset of all traks and new moov atom size
     */

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0,
                   "mp4 co64 atom adjustment");

    data = trak->out[NGX_HTTP_MP4_CO64_DATA].buf;
    entry = (uint64_t *) data->pos;
    end = (uint64_t *) data->last;

    while (entry < end) {
        offset = ngx_mp4_get_64value(entry);
        offset += adjustment;
        ngx_mp4_set_64value(entry, offset);
        entry++;
    }
}


static char *
ngx_http_mp4(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_core_loc_conf_t  *clcf;

    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
    clcf->handler = ngx_http_mp4_handler;

    return NGX_CONF_OK;
}


static void *
ngx_http_mp4_create_conf(ngx_conf_t *cf)
{
    ngx_http_mp4_conf_t  *conf;

    conf = ngx_palloc(cf->pool, sizeof(ngx_http_mp4_conf_t));
    if (conf == NULL) {
        return NULL;
    }

    conf->buffer_size = NGX_CONF_UNSET_SIZE;
    conf->max_buffer_size = NGX_CONF_UNSET_SIZE;
    conf->start_key_frame = NGX_CONF_UNSET;

    return conf;
}


static char *
ngx_http_mp4_merge_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_mp4_conf_t *prev = parent;
    ngx_http_mp4_conf_t *conf = child;

    ngx_conf_merge_size_value(conf->buffer_size, prev->buffer_size, 512 * 1024);
    ngx_conf_merge_size_value(conf->max_buffer_size, prev->max_buffer_size,
                              10 * 1024 * 1024);
    ngx_conf_merge_value(conf->start_key_frame, prev->start_key_frame, 0);

    return NGX_CONF_OK;
}