3502 lines
No EOL
122 KiB
C
3502 lines
No EOL
122 KiB
C
#ifndef MINIMP4_H
|
|
#define MINIMP4_H
|
|
/*
|
|
https://github.com/aspt/mp4
|
|
https://github.com/lieff/minimp4
|
|
To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide.
|
|
This software is distributed without any warranty.
|
|
See <http://creativecommons.org/publicdomain/zero/1.0/>.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <limits.h>
|
|
#include <assert.h>
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
#define MINIMP4_MIN(x, y) ((x) < (y) ? (x) : (y))
|
|
|
|
/************************************************************************/
|
|
/* Build configuration */
|
|
/************************************************************************/
|
|
|
|
#define FIX_BAD_ANDROID_META_BOX 1
|
|
|
|
#define MAX_CHUNKS_DEPTH 64 // Max chunks nesting level
|
|
|
|
#define MINIMP4_MAX_SPS 32
|
|
#define MINIMP4_MAX_PPS 256
|
|
|
|
#define MINIMP4_TRANSCODE_SPS_ID 1
|
|
|
|
// Support indexing of MP4 files over 4 GB.
|
|
// If disabled, files with 64-bit offset fields is still supported,
|
|
// but error signaled if such field contains too big offset
|
|
// This switch affect return type of MP4D_frame_offset() function
|
|
#define MINIMP4_ALLOW_64BIT 1
|
|
|
|
#define MP4D_TRACE_SUPPORTED 0 // Debug trace
|
|
#define MP4D_TRACE_TIMESTAMPS 1
|
|
// Support parsing of supplementary information, not necessary for decoding:
|
|
// duration, language, bitrate, metadata tags, etc
|
|
#define MP4D_INFO_SUPPORTED 1
|
|
|
|
// Enable code, which prints to stdout supplementary MP4 information:
|
|
#define MP4D_PRINT_INFO_SUPPORTED 0
|
|
|
|
#define MP4D_AVC_SUPPORTED 1
|
|
#define MP4D_HEVC_SUPPORTED 1
|
|
#define MP4D_TIMESTAMPS_SUPPORTED 1
|
|
|
|
// Enable TrackFragmentBaseMediaDecodeTimeBox support
|
|
#define MP4D_TFDT_SUPPORT 0
|
|
|
|
/************************************************************************/
|
|
/* Some values of MP4(E/D)_track_t->object_type_indication */
|
|
/************************************************************************/
|
|
// MPEG-4 AAC (all profiles)
|
|
#define MP4_OBJECT_TYPE_AUDIO_ISO_IEC_14496_3 0x40
|
|
// MPEG-2 AAC, Main profile
|
|
#define MP4_OBJECT_TYPE_AUDIO_ISO_IEC_13818_7_MAIN_PROFILE 0x66
|
|
// MPEG-2 AAC, LC profile
|
|
#define MP4_OBJECT_TYPE_AUDIO_ISO_IEC_13818_7_LC_PROFILE 0x67
|
|
// MPEG-2 AAC, SSR profile
|
|
#define MP4_OBJECT_TYPE_AUDIO_ISO_IEC_13818_7_SSR_PROFILE 0x68
|
|
// H.264 (AVC) video
|
|
#define MP4_OBJECT_TYPE_AVC 0x21
|
|
// H.265 (HEVC) video
|
|
#define MP4_OBJECT_TYPE_HEVC 0x23
|
|
// http://www.mp4ra.org/object.html 0xC0-E0 && 0xE2 - 0xFE are specified as "user private"
|
|
#define MP4_OBJECT_TYPE_USER_PRIVATE 0xC0
|
|
|
|
/************************************************************************/
|
|
/* API error codes */
|
|
/************************************************************************/
|
|
#define MP4E_STATUS_OK 0
|
|
#define MP4E_STATUS_BAD_ARGUMENTS -1
|
|
#define MP4E_STATUS_NO_MEMORY -2
|
|
#define MP4E_STATUS_FILE_WRITE_ERROR -3
|
|
#define MP4E_STATUS_ONLY_ONE_DSI_ALLOWED -4
|
|
|
|
/************************************************************************/
|
|
/* Sample kind for MP4E_put_sample() */
|
|
/************************************************************************/
|
|
#define MP4E_SAMPLE_DEFAULT 0 // (beginning of) audio or video frame
|
|
#define MP4E_SAMPLE_RANDOM_ACCESS 1 // mark sample as random access point (key frame)
|
|
#define MP4E_SAMPLE_CONTINUATION 2 // Not a sample, but continuation of previous sample (new slice)
|
|
|
|
/************************************************************************/
|
|
/* Portable 64-bit type definition */
|
|
/************************************************************************/
|
|
|
|
#if MINIMP4_ALLOW_64BIT
|
|
typedef uint64_t boxsize_t;
|
|
#else
|
|
typedef unsigned int boxsize_t;
|
|
#endif
|
|
typedef boxsize_t MP4D_file_offset_t;
|
|
|
|
/************************************************************************/
|
|
/* Some values of MP4D_track_t->handler_type */
|
|
/************************************************************************/
|
|
// Video track : 'vide'
|
|
#define MP4D_HANDLER_TYPE_VIDE 0x76696465
|
|
// Audio track : 'soun'
|
|
#define MP4D_HANDLER_TYPE_SOUN 0x736F756E
|
|
// General MPEG-4 systems streams (without specific handler).
|
|
// Used for private stream, as suggested in http://www.mp4ra.org/handler.html
|
|
#define MP4E_HANDLER_TYPE_GESM 0x6765736D
|
|
|
|
|
|
#define HEVC_NAL_VPS 32
|
|
#define HEVC_NAL_SPS 33
|
|
#define HEVC_NAL_PPS 34
|
|
#define HEVC_NAL_BLA_W_LP 16
|
|
#define HEVC_NAL_CRA_NUT 21
|
|
|
|
/************************************************************************/
|
|
/* Data structures */
|
|
/************************************************************************/
|
|
|
|
typedef struct MP4E_mux_tag MP4E_mux_t;
|
|
|
|
typedef enum
|
|
{
|
|
e_audio,
|
|
e_video,
|
|
e_private
|
|
} track_media_kind_t;
|
|
|
|
typedef struct
|
|
{
|
|
// MP4 object type code, which defined codec class for the track.
|
|
// See MP4E_OBJECT_TYPE_* values for some codecs
|
|
unsigned object_type_indication;
|
|
|
|
// Track language: 3-char ISO 639-2T code: "und", "eng", "rus", "jpn" etc...
|
|
unsigned char language[4];
|
|
|
|
track_media_kind_t track_media_kind;
|
|
|
|
// 90000 for video, sample rate for audio
|
|
unsigned time_scale;
|
|
unsigned default_duration;
|
|
|
|
union
|
|
{
|
|
struct
|
|
{
|
|
// number of channels in the audio track.
|
|
unsigned channelcount;
|
|
} a;
|
|
|
|
struct
|
|
{
|
|
int width;
|
|
int height;
|
|
} v;
|
|
} u;
|
|
|
|
} MP4E_track_t;
|
|
|
|
typedef struct MP4D_sample_to_chunk_t_tag MP4D_sample_to_chunk_t;
|
|
|
|
typedef struct
|
|
{
|
|
/************************************************************************/
|
|
/* mandatory public data */
|
|
/************************************************************************/
|
|
// How many 'samples' in the track
|
|
// The 'sample' is MP4 term, denoting audio or video frame
|
|
unsigned sample_count;
|
|
|
|
// Decoder-specific info (DSI) data
|
|
unsigned char *dsi;
|
|
|
|
// DSI data size
|
|
unsigned dsi_bytes;
|
|
|
|
// MP4 object type code
|
|
// case 0x00: return "Forbidden";
|
|
// case 0x01: return "Systems ISO/IEC 14496-1";
|
|
// case 0x02: return "Systems ISO/IEC 14496-1";
|
|
// case 0x20: return "Visual ISO/IEC 14496-2";
|
|
// case 0x40: return "Audio ISO/IEC 14496-3";
|
|
// case 0x60: return "Visual ISO/IEC 13818-2 Simple Profile";
|
|
// case 0x61: return "Visual ISO/IEC 13818-2 Main Profile";
|
|
// case 0x62: return "Visual ISO/IEC 13818-2 SNR Profile";
|
|
// case 0x63: return "Visual ISO/IEC 13818-2 Spatial Profile";
|
|
// case 0x64: return "Visual ISO/IEC 13818-2 High Profile";
|
|
// case 0x65: return "Visual ISO/IEC 13818-2 422 Profile";
|
|
// case 0x66: return "Audio ISO/IEC 13818-7 Main Profile";
|
|
// case 0x67: return "Audio ISO/IEC 13818-7 LC Profile";
|
|
// case 0x68: return "Audio ISO/IEC 13818-7 SSR Profile";
|
|
// case 0x69: return "Audio ISO/IEC 13818-3";
|
|
// case 0x6A: return "Visual ISO/IEC 11172-2";
|
|
// case 0x6B: return "Audio ISO/IEC 11172-3";
|
|
// case 0x6C: return "Visual ISO/IEC 10918-1";
|
|
unsigned object_type_indication;
|
|
|
|
#if MP4D_INFO_SUPPORTED
|
|
/************************************************************************/
|
|
/* informational public data */
|
|
/************************************************************************/
|
|
// handler_type when present in a media box, is an integer containing one of
|
|
// the following values, or a value from a derived specification:
|
|
// 'vide' Video track
|
|
// 'soun' Audio track
|
|
// 'hint' Hint track
|
|
unsigned handler_type;
|
|
|
|
// Track duration: 64-bit value split into 2 variables
|
|
unsigned duration_hi;
|
|
unsigned duration_lo;
|
|
|
|
// duration scale: duration = timescale*seconds
|
|
unsigned timescale;
|
|
|
|
// Average bitrate, bits per second
|
|
unsigned avg_bitrate_bps;
|
|
|
|
// Track language: 3-char ISO 639-2T code: "und", "eng", "rus", "jpn" etc...
|
|
unsigned char language[4];
|
|
|
|
// MP4 stream type
|
|
// case 0x00: return "Forbidden";
|
|
// case 0x01: return "ObjectDescriptorStream";
|
|
// case 0x02: return "ClockReferenceStream";
|
|
// case 0x03: return "SceneDescriptionStream";
|
|
// case 0x04: return "VisualStream";
|
|
// case 0x05: return "AudioStream";
|
|
// case 0x06: return "MPEG7Stream";
|
|
// case 0x07: return "IPMPStream";
|
|
// case 0x08: return "ObjectContentInfoStream";
|
|
// case 0x09: return "MPEGJStream";
|
|
unsigned stream_type;
|
|
|
|
union
|
|
{
|
|
// for handler_type == 'soun' tracks
|
|
struct
|
|
{
|
|
unsigned channelcount;
|
|
unsigned samplerate_hz;
|
|
} audio;
|
|
|
|
// for handler_type == 'vide' tracks
|
|
struct
|
|
{
|
|
unsigned width;
|
|
unsigned height;
|
|
} video;
|
|
} SampleDescription;
|
|
#endif
|
|
|
|
/************************************************************************/
|
|
/* private data: MP4 indexes */
|
|
/************************************************************************/
|
|
unsigned *entry_size;
|
|
|
|
unsigned sample_to_chunk_count;
|
|
struct MP4D_sample_to_chunk_t_tag *sample_to_chunk;
|
|
|
|
unsigned chunk_count;
|
|
MP4D_file_offset_t *chunk_offset;
|
|
|
|
#if MP4D_TIMESTAMPS_SUPPORTED
|
|
unsigned *timestamp;
|
|
unsigned *duration;
|
|
#endif
|
|
|
|
} MP4D_track_t;
|
|
|
|
typedef struct MP4D_demux_tag
|
|
{
|
|
/************************************************************************/
|
|
/* mandatory public data */
|
|
/************************************************************************/
|
|
int64_t read_pos;
|
|
int64_t read_size;
|
|
MP4D_track_t *track;
|
|
int (*read_callback)(int64_t offset, void *buffer, size_t size, void *token);
|
|
void *token;
|
|
|
|
unsigned track_count; // number of tracks in the movie
|
|
|
|
#if MP4D_INFO_SUPPORTED
|
|
/************************************************************************/
|
|
/* informational public data */
|
|
/************************************************************************/
|
|
// Movie duration: 64-bit value split into 2 variables
|
|
unsigned duration_hi;
|
|
unsigned duration_lo;
|
|
|
|
// duration scale: duration = timescale*seconds
|
|
unsigned timescale;
|
|
|
|
// Metadata tag (optional)
|
|
// Tags provided 'as-is', without any re-encoding
|
|
struct
|
|
{
|
|
unsigned char *title;
|
|
unsigned char *artist;
|
|
unsigned char *album;
|
|
unsigned char *year;
|
|
unsigned char *comment;
|
|
unsigned char *genre;
|
|
} tag;
|
|
#endif
|
|
|
|
} MP4D_demux_t;
|
|
|
|
struct MP4D_sample_to_chunk_t_tag
|
|
{
|
|
unsigned first_chunk;
|
|
unsigned samples_per_chunk;
|
|
};
|
|
|
|
typedef struct
|
|
{
|
|
void *sps_cache[MINIMP4_MAX_SPS];
|
|
void *pps_cache[MINIMP4_MAX_PPS];
|
|
int sps_bytes[MINIMP4_MAX_SPS];
|
|
int pps_bytes[MINIMP4_MAX_PPS];
|
|
|
|
int map_sps[MINIMP4_MAX_SPS];
|
|
int map_pps[MINIMP4_MAX_PPS];
|
|
|
|
} h264_sps_id_patcher_t;
|
|
|
|
typedef struct mp4_h26x_writer_tag
|
|
{
|
|
#if MINIMP4_TRANSCODE_SPS_ID
|
|
h264_sps_id_patcher_t sps_patcher;
|
|
#endif
|
|
MP4E_mux_t *mux;
|
|
int mux_track_id, is_hevc, need_vps, need_sps, need_pps, need_idr;
|
|
} mp4_h26x_writer_t;
|
|
|
|
int mp4_h26x_write_init(mp4_h26x_writer_t *h, MP4E_mux_t *mux, int width, int height, int is_hevc);
|
|
void mp4_h26x_write_close(mp4_h26x_writer_t *h);
|
|
int mp4_h26x_write_nal(mp4_h26x_writer_t *h, const unsigned char *nal, int length, unsigned timeStamp90kHz_next);
|
|
|
|
/************************************************************************/
|
|
/* API */
|
|
/************************************************************************/
|
|
|
|
/**
|
|
* Parse given input stream as MP4 file. Allocate and store data indexes.
|
|
* return 1 on success, 0 on failure
|
|
* The MP4 indexes may be stored at the end of stream, so this
|
|
* function may parse all stream.
|
|
* It is guaranteed that function will read/seek sequentially,
|
|
* and will never jump back.
|
|
*/
|
|
int MP4D_open(MP4D_demux_t *mp4, int (*read_callback)(int64_t offset, void *buffer, size_t size, void *token), void *token, int64_t file_size);
|
|
|
|
/**
|
|
* Return position and size for given sample from given track. The 'sample' is a
|
|
* MP4 term for 'frame'
|
|
*
|
|
* frame_bytes [OUT] - return coded frame size in bytes
|
|
* timestamp [OUT] - return frame timestamp (in mp4->timescale units)
|
|
* duration [OUT] - return frame duration (in mp4->timescale units)
|
|
*
|
|
* function return offset for the frame
|
|
*/
|
|
MP4D_file_offset_t MP4D_frame_offset(const MP4D_demux_t *mp4, unsigned int ntrack, unsigned int nsample, unsigned int *frame_bytes, unsigned *timestamp, unsigned *duration);
|
|
|
|
/**
|
|
* De-allocated memory
|
|
*/
|
|
void MP4D_close(MP4D_demux_t *mp4);
|
|
|
|
/**
|
|
* Helper functions to parse mp4.track[ntrack].dsi for H.264 SPS/PPS
|
|
* Return pointer to internal mp4 memory, it must not be free()-ed
|
|
*
|
|
* Example: process all SPS in MP4 file:
|
|
* while (sps = MP4D_read_sps(mp4, num_of_avc_track, sps_count, &sps_bytes))
|
|
* {
|
|
* process(sps, sps_bytes);
|
|
* sps_count++;
|
|
* }
|
|
*/
|
|
const void *MP4D_read_sps(const MP4D_demux_t *mp4, unsigned int ntrack, int nsps, int *sps_bytes);
|
|
const void *MP4D_read_pps(const MP4D_demux_t *mp4, unsigned int ntrack, int npps, int *pps_bytes);
|
|
|
|
#if MP4D_PRINT_INFO_SUPPORTED
|
|
/**
|
|
* Print MP4 information to stdout.
|
|
* Uses printf() as well as floating-point functions
|
|
* Given as implementation example and for test purposes
|
|
*/
|
|
void MP4D_printf_info(const MP4D_demux_t *mp4);
|
|
#endif
|
|
|
|
/**
|
|
* Allocates and initialize mp4 multiplexor
|
|
* Given file handler is transparent to the MP4 library, and used only as
|
|
* argument for given fwrite_callback() function. By appropriate definition
|
|
* of callback function application may use any other file output API (for
|
|
* example C++ streams, or Win32 file functions)
|
|
*
|
|
* return multiplexor handle on success; NULL on failure
|
|
*/
|
|
MP4E_mux_t *MP4E_open(int sequential_mode_flag, int enable_fragmentation, void *token,
|
|
int (*write_callback)(int64_t offset, const void *buffer, size_t size, void *token));
|
|
|
|
/**
|
|
* Add new track
|
|
* The track_data parameter does not referred by the multiplexer after function
|
|
* return, and may be allocated in short-time memory. The dsi member of
|
|
* track_data parameter is mandatory.
|
|
*
|
|
* return ID of added track, or error code MP4E_STATUS_*
|
|
*/
|
|
int MP4E_add_track(MP4E_mux_t *mux, const MP4E_track_t *track_data);
|
|
|
|
/**
|
|
* Add new sample to specified track
|
|
* The tracks numbered starting with 0, according to order of MP4E_add_track() calls
|
|
* 'kind' is one of MP4E_SAMPLE_... defines
|
|
*
|
|
* return error code MP4E_STATUS_*
|
|
*
|
|
* Example:
|
|
* MP4E_put_sample(mux, 0, data, data_bytes, duration, MP4E_SAMPLE_DEFAULT);
|
|
*/
|
|
int MP4E_put_sample(MP4E_mux_t *mux, int track_num, const void *data, int data_bytes, int duration, int kind);
|
|
|
|
/**
|
|
* Finalize MP4 file, de-allocated memory, and closes MP4 multiplexer.
|
|
* The close operation takes a time and disk space, since it writes MP4 file
|
|
* indexes. Please note that this function does not closes file handle,
|
|
* which was passed to open function.
|
|
*
|
|
* return error code MP4E_STATUS_*
|
|
*/
|
|
int MP4E_close(MP4E_mux_t *mux);
|
|
|
|
/**
|
|
* Set Decoder Specific Info (DSI)
|
|
* Can be used for audio and private tracks.
|
|
* MUST be used for AAC track.
|
|
* Only one DSI can be set. It is an error to set DSI again
|
|
*
|
|
* return error code MP4E_STATUS_*
|
|
*/
|
|
int MP4E_set_dsi(MP4E_mux_t *mux, int track_id, const void *dsi, int bytes);
|
|
|
|
/**
|
|
* Set VPS data. MUST be used for HEVC (H.265) track.
|
|
*
|
|
* return error code MP4E_STATUS_*
|
|
*/
|
|
int MP4E_set_vps(MP4E_mux_t *mux, int track_id, const void *vps, int bytes);
|
|
|
|
/**
|
|
* Set SPS data. MUST be used for AVC (H.264) track. Up to 32 different SPS can be used in one track.
|
|
*
|
|
* return error code MP4E_STATUS_*
|
|
*/
|
|
int MP4E_set_sps(MP4E_mux_t *mux, int track_id, const void *sps, int bytes);
|
|
|
|
/**
|
|
* Set PPS data. MUST be used for AVC (H.264) track. Up to 256 different PPS can be used in one track.
|
|
*
|
|
* return error code MP4E_STATUS_*
|
|
*/
|
|
int MP4E_set_pps(MP4E_mux_t *mux, int track_id, const void *pps, int bytes);
|
|
|
|
/**
|
|
* Set or replace ASCII test comment for the file. Set comment to NULL to remove comment.
|
|
*
|
|
* return error code MP4E_STATUS_*
|
|
*/
|
|
int MP4E_set_text_comment(MP4E_mux_t *mux, const char *comment);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
#endif //MINIMP4_H
|
|
|
|
#if defined(MINIMP4_IMPLEMENTATION) && !defined(MINIMP4_IMPLEMENTATION_GUARD)
|
|
#define MINIMP4_IMPLEMENTATION_GUARD
|
|
|
|
#define FOUR_CHAR_INT(a, b, c, d) (((uint32_t)(a) << 24) | ((b) << 16) | ((c) << 8) | (d))
|
|
enum
|
|
{
|
|
BOX_co64 = FOUR_CHAR_INT( 'c', 'o', '6', '4' ),//ChunkLargeOffsetAtomType
|
|
BOX_stco = FOUR_CHAR_INT( 's', 't', 'c', 'o' ),//ChunkOffsetAtomType
|
|
BOX_crhd = FOUR_CHAR_INT( 'c', 'r', 'h', 'd' ),//ClockReferenceMediaHeaderAtomType
|
|
BOX_ctts = FOUR_CHAR_INT( 'c', 't', 't', 's' ),//CompositionOffsetAtomType
|
|
BOX_cprt = FOUR_CHAR_INT( 'c', 'p', 'r', 't' ),//CopyrightAtomType
|
|
BOX_url_ = FOUR_CHAR_INT( 'u', 'r', 'l', ' ' ),//DataEntryURLAtomType
|
|
BOX_urn_ = FOUR_CHAR_INT( 'u', 'r', 'n', ' ' ),//DataEntryURNAtomType
|
|
BOX_dinf = FOUR_CHAR_INT( 'd', 'i', 'n', 'f' ),//DataInformationAtomType
|
|
BOX_dref = FOUR_CHAR_INT( 'd', 'r', 'e', 'f' ),//DataReferenceAtomType
|
|
BOX_stdp = FOUR_CHAR_INT( 's', 't', 'd', 'p' ),//DegradationPriorityAtomType
|
|
BOX_edts = FOUR_CHAR_INT( 'e', 'd', 't', 's' ),//EditAtomType
|
|
BOX_elst = FOUR_CHAR_INT( 'e', 'l', 's', 't' ),//EditListAtomType
|
|
BOX_uuid = FOUR_CHAR_INT( 'u', 'u', 'i', 'd' ),//ExtendedAtomType
|
|
BOX_free = FOUR_CHAR_INT( 'f', 'r', 'e', 'e' ),//FreeSpaceAtomType
|
|
BOX_hdlr = FOUR_CHAR_INT( 'h', 'd', 'l', 'r' ),//HandlerAtomType
|
|
BOX_hmhd = FOUR_CHAR_INT( 'h', 'm', 'h', 'd' ),//HintMediaHeaderAtomType
|
|
BOX_hint = FOUR_CHAR_INT( 'h', 'i', 'n', 't' ),//HintTrackReferenceAtomType
|
|
BOX_mdia = FOUR_CHAR_INT( 'm', 'd', 'i', 'a' ),//MediaAtomType
|
|
BOX_mdat = FOUR_CHAR_INT( 'm', 'd', 'a', 't' ),//MediaDataAtomType
|
|
BOX_mdhd = FOUR_CHAR_INT( 'm', 'd', 'h', 'd' ),//MediaHeaderAtomType
|
|
BOX_minf = FOUR_CHAR_INT( 'm', 'i', 'n', 'f' ),//MediaInformationAtomType
|
|
BOX_moov = FOUR_CHAR_INT( 'm', 'o', 'o', 'v' ),//MovieAtomType
|
|
BOX_mvhd = FOUR_CHAR_INT( 'm', 'v', 'h', 'd' ),//MovieHeaderAtomType
|
|
BOX_stsd = FOUR_CHAR_INT( 's', 't', 's', 'd' ),//SampleDescriptionAtomType
|
|
BOX_stsz = FOUR_CHAR_INT( 's', 't', 's', 'z' ),//SampleSizeAtomType
|
|
BOX_stz2 = FOUR_CHAR_INT( 's', 't', 'z', '2' ),//CompactSampleSizeAtomType
|
|
BOX_stbl = FOUR_CHAR_INT( 's', 't', 'b', 'l' ),//SampleTableAtomType
|
|
BOX_stsc = FOUR_CHAR_INT( 's', 't', 's', 'c' ),//SampleToChunkAtomType
|
|
BOX_stsh = FOUR_CHAR_INT( 's', 't', 's', 'h' ),//ShadowSyncAtomType
|
|
BOX_skip = FOUR_CHAR_INT( 's', 'k', 'i', 'p' ),//SkipAtomType
|
|
BOX_smhd = FOUR_CHAR_INT( 's', 'm', 'h', 'd' ),//SoundMediaHeaderAtomType
|
|
BOX_stss = FOUR_CHAR_INT( 's', 't', 's', 's' ),//SyncSampleAtomType
|
|
BOX_stts = FOUR_CHAR_INT( 's', 't', 't', 's' ),//TimeToSampleAtomType
|
|
BOX_trak = FOUR_CHAR_INT( 't', 'r', 'a', 'k' ),//TrackAtomType
|
|
BOX_tkhd = FOUR_CHAR_INT( 't', 'k', 'h', 'd' ),//TrackHeaderAtomType
|
|
BOX_tref = FOUR_CHAR_INT( 't', 'r', 'e', 'f' ),//TrackReferenceAtomType
|
|
BOX_udta = FOUR_CHAR_INT( 'u', 'd', 't', 'a' ),//UserDataAtomType
|
|
BOX_vmhd = FOUR_CHAR_INT( 'v', 'm', 'h', 'd' ),//VideoMediaHeaderAtomType
|
|
BOX_url = FOUR_CHAR_INT( 'u', 'r', 'l', ' ' ),
|
|
BOX_urn = FOUR_CHAR_INT( 'u', 'r', 'n', ' ' ),
|
|
|
|
BOX_gnrv = FOUR_CHAR_INT( 'g', 'n', 'r', 'v' ),//GenericVisualSampleEntryAtomType
|
|
BOX_gnra = FOUR_CHAR_INT( 'g', 'n', 'r', 'a' ),//GenericAudioSampleEntryAtomType
|
|
|
|
//V2 atoms
|
|
BOX_ftyp = FOUR_CHAR_INT( 'f', 't', 'y', 'p' ),//FileTypeAtomType
|
|
BOX_padb = FOUR_CHAR_INT( 'p', 'a', 'd', 'b' ),//PaddingBitsAtomType
|
|
|
|
//MP4 Atoms
|
|
BOX_sdhd = FOUR_CHAR_INT( 's', 'd', 'h', 'd' ),//SceneDescriptionMediaHeaderAtomType
|
|
BOX_dpnd = FOUR_CHAR_INT( 'd', 'p', 'n', 'd' ),//StreamDependenceAtomType
|
|
BOX_iods = FOUR_CHAR_INT( 'i', 'o', 'd', 's' ),//ObjectDescriptorAtomType
|
|
BOX_odhd = FOUR_CHAR_INT( 'o', 'd', 'h', 'd' ),//ObjectDescriptorMediaHeaderAtomType
|
|
BOX_mpod = FOUR_CHAR_INT( 'm', 'p', 'o', 'd' ),//ODTrackReferenceAtomType
|
|
BOX_nmhd = FOUR_CHAR_INT( 'n', 'm', 'h', 'd' ),//MPEGMediaHeaderAtomType
|
|
BOX_esds = FOUR_CHAR_INT( 'e', 's', 'd', 's' ),//ESDAtomType
|
|
BOX_sync = FOUR_CHAR_INT( 's', 'y', 'n', 'c' ),//OCRReferenceAtomType
|
|
BOX_ipir = FOUR_CHAR_INT( 'i', 'p', 'i', 'r' ),//IPIReferenceAtomType
|
|
BOX_mp4s = FOUR_CHAR_INT( 'm', 'p', '4', 's' ),//MPEGSampleEntryAtomType
|
|
BOX_mp4a = FOUR_CHAR_INT( 'm', 'p', '4', 'a' ),//MPEGAudioSampleEntryAtomType
|
|
BOX_mp4v = FOUR_CHAR_INT( 'm', 'p', '4', 'v' ),//MPEGVisualSampleEntryAtomType
|
|
|
|
// http://www.itscj.ipsj.or.jp/sc29/open/29view/29n7644t.doc
|
|
BOX_avc1 = FOUR_CHAR_INT( 'a', 'v', 'c', '1' ),
|
|
BOX_avc2 = FOUR_CHAR_INT( 'a', 'v', 'c', '2' ),
|
|
BOX_svc1 = FOUR_CHAR_INT( 's', 'v', 'c', '1' ),
|
|
BOX_avcC = FOUR_CHAR_INT( 'a', 'v', 'c', 'C' ),
|
|
BOX_svcC = FOUR_CHAR_INT( 's', 'v', 'c', 'C' ),
|
|
BOX_btrt = FOUR_CHAR_INT( 'b', 't', 'r', 't' ),
|
|
BOX_m4ds = FOUR_CHAR_INT( 'm', '4', 'd', 's' ),
|
|
BOX_seib = FOUR_CHAR_INT( 's', 'e', 'i', 'b' ),
|
|
|
|
// H264/HEVC
|
|
BOX_hev1 = FOUR_CHAR_INT( 'h', 'e', 'v', '1' ),
|
|
BOX_hvc1 = FOUR_CHAR_INT( 'h', 'v', 'c', '1' ),
|
|
BOX_hvcC = FOUR_CHAR_INT( 'h', 'v', 'c', 'C' ),
|
|
|
|
//3GPP atoms
|
|
BOX_samr = FOUR_CHAR_INT( 's', 'a', 'm', 'r' ),//AMRSampleEntryAtomType
|
|
BOX_sawb = FOUR_CHAR_INT( 's', 'a', 'w', 'b' ),//WB_AMRSampleEntryAtomType
|
|
BOX_damr = FOUR_CHAR_INT( 'd', 'a', 'm', 'r' ),//AMRConfigAtomType
|
|
BOX_s263 = FOUR_CHAR_INT( 's', '2', '6', '3' ),//H263SampleEntryAtomType
|
|
BOX_d263 = FOUR_CHAR_INT( 'd', '2', '6', '3' ),//H263ConfigAtomType
|
|
|
|
//V2 atoms - Movie Fragments
|
|
BOX_mvex = FOUR_CHAR_INT( 'm', 'v', 'e', 'x' ),//MovieExtendsAtomType
|
|
BOX_trex = FOUR_CHAR_INT( 't', 'r', 'e', 'x' ),//TrackExtendsAtomType
|
|
BOX_moof = FOUR_CHAR_INT( 'm', 'o', 'o', 'f' ),//MovieFragmentAtomType
|
|
BOX_mfhd = FOUR_CHAR_INT( 'm', 'f', 'h', 'd' ),//MovieFragmentHeaderAtomType
|
|
BOX_traf = FOUR_CHAR_INT( 't', 'r', 'a', 'f' ),//TrackFragmentAtomType
|
|
BOX_tfhd = FOUR_CHAR_INT( 't', 'f', 'h', 'd' ),//TrackFragmentHeaderAtomType
|
|
BOX_tfdt = FOUR_CHAR_INT( 't', 'f', 'd', 't' ),//TrackFragmentBaseMediaDecodeTimeBox
|
|
BOX_trun = FOUR_CHAR_INT( 't', 'r', 'u', 'n' ),//TrackFragmentRunAtomType
|
|
BOX_mehd = FOUR_CHAR_INT( 'm', 'e', 'h', 'd' ),//MovieExtendsHeaderBox
|
|
|
|
// Object Descriptors (OD) data coding
|
|
// These takes only 1 byte; this implementation translate <od_tag> to
|
|
// <od_tag> + OD_BASE to keep API uniform and safe for string functions
|
|
OD_BASE = FOUR_CHAR_INT( '$', '$', '$', '0' ),//
|
|
OD_ESD = FOUR_CHAR_INT( '$', '$', '$', '3' ),//SDescriptor_Tag
|
|
OD_DCD = FOUR_CHAR_INT( '$', '$', '$', '4' ),//DecoderConfigDescriptor_Tag
|
|
OD_DSI = FOUR_CHAR_INT( '$', '$', '$', '5' ),//DecoderSpecificInfo_Tag
|
|
OD_SLC = FOUR_CHAR_INT( '$', '$', '$', '6' ),//SLConfigDescriptor_Tag
|
|
|
|
BOX_meta = FOUR_CHAR_INT( 'm', 'e', 't', 'a' ),
|
|
BOX_ilst = FOUR_CHAR_INT( 'i', 'l', 's', 't' ),
|
|
|
|
// Metagata tags, see http://atomicparsley.sourceforge.net/mpeg-4files.html
|
|
BOX_calb = FOUR_CHAR_INT( '\xa9', 'a', 'l', 'b'), // album
|
|
BOX_cart = FOUR_CHAR_INT( '\xa9', 'a', 'r', 't'), // artist
|
|
BOX_aART = FOUR_CHAR_INT( 'a', 'A', 'R', 'T' ), // album artist
|
|
BOX_ccmt = FOUR_CHAR_INT( '\xa9', 'c', 'm', 't'), // comment
|
|
BOX_cday = FOUR_CHAR_INT( '\xa9', 'd', 'a', 'y'), // year (as string)
|
|
BOX_cnam = FOUR_CHAR_INT( '\xa9', 'n', 'a', 'm'), // title
|
|
BOX_cgen = FOUR_CHAR_INT( '\xa9', 'g', 'e', 'n'), // custom genre (as string or as byte!)
|
|
BOX_trkn = FOUR_CHAR_INT( 't', 'r', 'k', 'n'), // track number (byte)
|
|
BOX_disk = FOUR_CHAR_INT( 'd', 'i', 's', 'k'), // disk number (byte)
|
|
BOX_cwrt = FOUR_CHAR_INT( '\xa9', 'w', 'r', 't'), // composer
|
|
BOX_ctoo = FOUR_CHAR_INT( '\xa9', 't', 'o', 'o'), // encoder
|
|
BOX_tmpo = FOUR_CHAR_INT( 't', 'm', 'p', 'o'), // bpm (byte)
|
|
BOX_cpil = FOUR_CHAR_INT( 'c', 'p', 'i', 'l'), // compilation (byte)
|
|
BOX_covr = FOUR_CHAR_INT( 'c', 'o', 'v', 'r'), // cover art (JPEG/PNG)
|
|
BOX_rtng = FOUR_CHAR_INT( 'r', 't', 'n', 'g'), // rating/advisory (byte)
|
|
BOX_cgrp = FOUR_CHAR_INT( '\xa9', 'g', 'r', 'p'), // grouping
|
|
BOX_stik = FOUR_CHAR_INT( 's', 't', 'i', 'k'), // stik (byte) 0 = Movie 1 = Normal 2 = Audiobook 5 = Whacked Bookmark 6 = Music Video 9 = Short Film 10 = TV Show 11 = Booklet 14 = Ringtone
|
|
BOX_pcst = FOUR_CHAR_INT( 'p', 'c', 's', 't'), // podcast (byte)
|
|
BOX_catg = FOUR_CHAR_INT( 'c', 'a', 't', 'g'), // category
|
|
BOX_keyw = FOUR_CHAR_INT( 'k', 'e', 'y', 'w'), // keyword
|
|
BOX_purl = FOUR_CHAR_INT( 'p', 'u', 'r', 'l'), // podcast URL (byte)
|
|
BOX_egid = FOUR_CHAR_INT( 'e', 'g', 'i', 'd'), // episode global unique ID (byte)
|
|
BOX_desc = FOUR_CHAR_INT( 'd', 'e', 's', 'c'), // description
|
|
BOX_clyr = FOUR_CHAR_INT( '\xa9', 'l', 'y', 'r'), // lyrics (may be > 255 bytes)
|
|
BOX_tven = FOUR_CHAR_INT( 't', 'v', 'e', 'n'), // tv episode number
|
|
BOX_tves = FOUR_CHAR_INT( 't', 'v', 'e', 's'), // tv episode (byte)
|
|
BOX_tvnn = FOUR_CHAR_INT( 't', 'v', 'n', 'n'), // tv network name
|
|
BOX_tvsh = FOUR_CHAR_INT( 't', 'v', 's', 'h'), // tv show name
|
|
BOX_tvsn = FOUR_CHAR_INT( 't', 'v', 's', 'n'), // tv season (byte)
|
|
BOX_purd = FOUR_CHAR_INT( 'p', 'u', 'r', 'd'), // purchase date
|
|
BOX_pgap = FOUR_CHAR_INT( 'p', 'g', 'a', 'p'), // Gapless Playback (byte)
|
|
|
|
//BOX_aart = FOUR_CHAR_INT( 'a', 'a', 'r', 't' ), // Album artist
|
|
BOX_cART = FOUR_CHAR_INT( '\xa9', 'A', 'R', 'T'), // artist
|
|
BOX_gnre = FOUR_CHAR_INT( 'g', 'n', 'r', 'e'),
|
|
|
|
// 3GPP metatags (http://cpansearch.perl.org/src/JHAR/MP4-Info-1.12/Info.pm)
|
|
BOX_auth = FOUR_CHAR_INT( 'a', 'u', 't', 'h'), // author
|
|
BOX_titl = FOUR_CHAR_INT( 't', 'i', 't', 'l'), // title
|
|
BOX_dscp = FOUR_CHAR_INT( 'd', 's', 'c', 'p'), // description
|
|
BOX_perf = FOUR_CHAR_INT( 'p', 'e', 'r', 'f'), // performer
|
|
BOX_mean = FOUR_CHAR_INT( 'm', 'e', 'a', 'n'), //
|
|
BOX_name = FOUR_CHAR_INT( 'n', 'a', 'm', 'e'), //
|
|
BOX_data = FOUR_CHAR_INT( 'd', 'a', 't', 'a'), //
|
|
|
|
// these from http://lists.mplayerhq.hu/pipermail/ffmpeg-devel/2008-September/053151.html
|
|
BOX_albm = FOUR_CHAR_INT( 'a', 'l', 'b', 'm'), // album
|
|
BOX_yrrc = FOUR_CHAR_INT( 'y', 'r', 'r', 'c') // album
|
|
};
|
|
|
|
// Video track : 'vide'
|
|
#define MP4E_HANDLER_TYPE_VIDE 0x76696465
|
|
// Audio track : 'soun'
|
|
#define MP4E_HANDLER_TYPE_SOUN 0x736F756E
|
|
// General MPEG-4 systems streams (without specific handler).
|
|
// Used for private stream, as suggested in http://www.mp4ra.org/handler.html
|
|
#define MP4E_HANDLER_TYPE_GESM 0x6765736D
|
|
|
|
typedef struct
|
|
{
|
|
boxsize_t size;
|
|
boxsize_t offset;
|
|
unsigned duration;
|
|
unsigned flag_random_access;
|
|
} sample_t;
|
|
|
|
typedef struct {
|
|
unsigned char *data;
|
|
int bytes;
|
|
int capacity;
|
|
} minimp4_vector_t;
|
|
|
|
typedef struct
|
|
{
|
|
MP4E_track_t info;
|
|
minimp4_vector_t smpl; // sample descriptor
|
|
minimp4_vector_t pending_sample;
|
|
|
|
minimp4_vector_t vsps; // or dsi for audio
|
|
minimp4_vector_t vpps; // not used for audio
|
|
minimp4_vector_t vvps; // used for HEVC
|
|
|
|
} track_t;
|
|
|
|
typedef struct MP4E_mux_tag
|
|
{
|
|
minimp4_vector_t tracks;
|
|
|
|
int64_t write_pos;
|
|
int (*write_callback)(int64_t offset, const void *buffer, size_t size, void *token);
|
|
void *token;
|
|
char *text_comment;
|
|
|
|
int sequential_mode_flag;
|
|
int enable_fragmentation; // flag, indicating streaming-friendly 'fragmentation' mode
|
|
int fragments_count; // # of fragments in 'fragmentation' mode
|
|
|
|
} MP4E_mux_t;
|
|
|
|
static const unsigned char box_ftyp[] = {
|
|
#if 1
|
|
0,0,0,0x18,'f','t','y','p',
|
|
'm','p','4','2',0,0,0,0,
|
|
'm','p','4','2','i','s','o','m',
|
|
#else
|
|
// as in ffmpeg
|
|
0,0,0,0x20,'f','t','y','p',
|
|
'i','s','o','m',0,0,2,0,
|
|
'm','p','4','1','i','s','o','m',
|
|
'i','s','o','2','a','v','c','1',
|
|
#endif
|
|
};
|
|
|
|
/**
|
|
* Endian-independent byte-write macros
|
|
*/
|
|
#define WR(x, n) *p++ = (unsigned char)((x) >> 8*n)
|
|
#define WRITE_1(x) WR(x, 0);
|
|
#define WRITE_2(x) WR(x, 1); WR(x, 0);
|
|
#define WRITE_3(x) WR(x, 2); WR(x, 1); WR(x, 0);
|
|
#define WRITE_4(x) WR(x, 3); WR(x, 2); WR(x, 1); WR(x, 0);
|
|
#define WR4(p, x) (p)[0] = (char)((x) >> 8*3); (p)[1] = (char)((x) >> 8*2); (p)[2] = (char)((x) >> 8*1); (p)[3] = (char)((x));
|
|
|
|
// Finish atom: update atom size field
|
|
#define END_ATOM --stack; WR4((unsigned char*)*stack, p - *stack);
|
|
|
|
// Initiate atom: save position of size field on stack
|
|
#define ATOM(x) *stack++ = p; p += 4; WRITE_4(x);
|
|
|
|
// Atom with 'FullAtomVersionFlags' field
|
|
#define ATOM_FULL(x, flag) ATOM(x); WRITE_4(flag);
|
|
|
|
#define ERR(func) { int err = func; if (err) return err; }
|
|
|
|
/**
|
|
Allocate vector with given size, return 1 on success, 0 on fail
|
|
*/
|
|
static int minimp4_vector_init(minimp4_vector_t *h, int capacity)
|
|
{
|
|
h->bytes = 0;
|
|
h->capacity = capacity;
|
|
h->data = capacity ? (unsigned char *)malloc(capacity) : NULL;
|
|
return !capacity || !!h->data;
|
|
}
|
|
|
|
/**
|
|
Deallocates vector memory
|
|
*/
|
|
static void minimp4_vector_reset(minimp4_vector_t *h)
|
|
{
|
|
if (h->data)
|
|
free(h->data);
|
|
memset(h, 0, sizeof(minimp4_vector_t));
|
|
}
|
|
|
|
/**
|
|
Reallocate vector memory to the given size
|
|
*/
|
|
static int minimp4_vector_grow(minimp4_vector_t *h, int bytes)
|
|
{
|
|
void *p;
|
|
int new_size = h->capacity*2 + 1024;
|
|
if (new_size < h->capacity + bytes)
|
|
new_size = h->capacity + bytes + 1024;
|
|
p = realloc(h->data, new_size);
|
|
if (!p)
|
|
return 0;
|
|
h->data = (unsigned char*)p;
|
|
h->capacity = new_size;
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
Allocates given number of bytes at the end of vector data, increasing
|
|
vector memory if necessary.
|
|
Return allocated memory.
|
|
*/
|
|
static unsigned char *minimp4_vector_alloc_tail(minimp4_vector_t *h, int bytes)
|
|
{
|
|
unsigned char *p;
|
|
if (!h->data && !minimp4_vector_init(h, 2*bytes + 1024))
|
|
return NULL;
|
|
if ((h->capacity - h->bytes) < bytes && !minimp4_vector_grow(h, bytes))
|
|
return NULL;
|
|
assert(h->data);
|
|
assert((h->capacity - h->bytes) >= bytes);
|
|
p = h->data + h->bytes;
|
|
h->bytes += bytes;
|
|
return p;
|
|
}
|
|
|
|
/**
|
|
Append data to the end of the vector (accumulate ot enqueue)
|
|
*/
|
|
static unsigned char *minimp4_vector_put(minimp4_vector_t *h, const void *buf, int bytes)
|
|
{
|
|
unsigned char *tail = minimp4_vector_alloc_tail(h, bytes);
|
|
if (tail)
|
|
memcpy(tail, buf, bytes);
|
|
return tail;
|
|
}
|
|
|
|
/**
|
|
* Allocates and initialize mp4 multiplexer
|
|
* return multiplexor handle on success; NULL on failure
|
|
*/
|
|
MP4E_mux_t *MP4E_open(int sequential_mode_flag, int enable_fragmentation, void *token,
|
|
int (*write_callback)(int64_t offset, const void *buffer, size_t size, void *token))
|
|
{
|
|
if (write_callback(0, box_ftyp, sizeof(box_ftyp), token)) // Write fixed header: 'ftyp' box
|
|
return 0;
|
|
MP4E_mux_t *mux = (MP4E_mux_t*)malloc(sizeof(MP4E_mux_t));
|
|
if (!mux)
|
|
return mux;
|
|
mux->sequential_mode_flag = sequential_mode_flag || enable_fragmentation;
|
|
mux->enable_fragmentation = enable_fragmentation;
|
|
mux->fragments_count = 0;
|
|
mux->write_callback = write_callback;
|
|
mux->token = token;
|
|
mux->text_comment = NULL;
|
|
mux->write_pos = sizeof(box_ftyp);
|
|
|
|
if (!mux->sequential_mode_flag)
|
|
{ // Write filler, which would be updated later
|
|
if (mux->write_callback(mux->write_pos, box_ftyp, 8, mux->token))
|
|
{
|
|
free(mux);
|
|
return 0;
|
|
}
|
|
mux->write_pos += 16; // box_ftyp + box_free for 32bit or 64bit size encoding
|
|
}
|
|
minimp4_vector_init(&mux->tracks, 2*sizeof(track_t));
|
|
return mux;
|
|
}
|
|
|
|
/**
|
|
* Add new track
|
|
*/
|
|
int MP4E_add_track(MP4E_mux_t *mux, const MP4E_track_t *track_data)
|
|
{
|
|
track_t *tr;
|
|
int ntr = mux->tracks.bytes / sizeof(track_t);
|
|
|
|
if (!mux || !track_data)
|
|
return MP4E_STATUS_BAD_ARGUMENTS;
|
|
|
|
tr = (track_t*)minimp4_vector_alloc_tail(&mux->tracks, sizeof(track_t));
|
|
if (!tr)
|
|
return MP4E_STATUS_NO_MEMORY;
|
|
memset(tr, 0, sizeof(track_t));
|
|
memcpy(&tr->info, track_data, sizeof(*track_data));
|
|
if (!minimp4_vector_init(&tr->smpl, 256))
|
|
return MP4E_STATUS_NO_MEMORY;
|
|
minimp4_vector_init(&tr->vsps, 0);
|
|
minimp4_vector_init(&tr->vpps, 0);
|
|
minimp4_vector_init(&tr->pending_sample, 0);
|
|
return ntr;
|
|
}
|
|
|
|
static const unsigned char *next_dsi(const unsigned char *p, const unsigned char *end, int *bytes)
|
|
{
|
|
if (p < end + 2)
|
|
{
|
|
*bytes = p[0]*256 + p[1];
|
|
return p + 2;
|
|
} else
|
|
return NULL;
|
|
}
|
|
|
|
static int append_mem(minimp4_vector_t *v, const void *mem, int bytes)
|
|
{
|
|
int i;
|
|
unsigned char size[2];
|
|
const unsigned char *p = v->data;
|
|
for (i = 0; i + 2 < v->bytes;)
|
|
{
|
|
int cb = p[i]*256 + p[i + 1];
|
|
if (cb == bytes && !memcmp(p + i + 2, mem, cb))
|
|
return 1;
|
|
i += 2 + cb;
|
|
}
|
|
size[0] = bytes >> 8;
|
|
size[1] = bytes;
|
|
return minimp4_vector_put(v, size, 2) && minimp4_vector_put(v, mem, bytes);
|
|
}
|
|
|
|
static int items_count(minimp4_vector_t *v)
|
|
{
|
|
int i, count = 0;
|
|
const unsigned char *p = v->data;
|
|
for (i = 0; i + 2 < v->bytes;)
|
|
{
|
|
int cb = p[i]*256 + p[i + 1];
|
|
count++;
|
|
i += 2 + cb;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
int MP4E_set_dsi(MP4E_mux_t *mux, int track_id, const void *dsi, int bytes)
|
|
{
|
|
track_t* tr = ((track_t*)mux->tracks.data) + track_id;
|
|
assert(tr->info.track_media_kind == e_audio ||
|
|
tr->info.track_media_kind == e_private);
|
|
if (tr->vsps.bytes)
|
|
return MP4E_STATUS_ONLY_ONE_DSI_ALLOWED; // only one DSI allowed
|
|
return append_mem(&tr->vsps, dsi, bytes) ? MP4E_STATUS_OK : MP4E_STATUS_NO_MEMORY;
|
|
}
|
|
|
|
int MP4E_set_vps(MP4E_mux_t *mux, int track_id, const void *vps, int bytes)
|
|
{
|
|
track_t* tr = ((track_t*)mux->tracks.data) + track_id;
|
|
assert(tr->info.track_media_kind == e_video);
|
|
return append_mem(&tr->vvps, vps, bytes) ? MP4E_STATUS_OK : MP4E_STATUS_NO_MEMORY;
|
|
}
|
|
|
|
int MP4E_set_sps(MP4E_mux_t *mux, int track_id, const void *sps, int bytes)
|
|
{
|
|
track_t* tr = ((track_t*)mux->tracks.data) + track_id;
|
|
assert(tr->info.track_media_kind == e_video);
|
|
return append_mem(&tr->vsps, sps, bytes) ? MP4E_STATUS_OK : MP4E_STATUS_NO_MEMORY;
|
|
}
|
|
|
|
int MP4E_set_pps(MP4E_mux_t *mux, int track_id, const void *pps, int bytes)
|
|
{
|
|
track_t* tr = ((track_t*)mux->tracks.data) + track_id;
|
|
assert(tr->info.track_media_kind == e_video);
|
|
return append_mem(&tr->vpps, pps, bytes) ? MP4E_STATUS_OK : MP4E_STATUS_NO_MEMORY;
|
|
}
|
|
|
|
static unsigned get_duration(const track_t *tr)
|
|
{
|
|
unsigned i, sum_duration = 0;
|
|
const sample_t *s = (const sample_t *)tr->smpl.data;
|
|
for (i = 0; i < tr->smpl.bytes/sizeof(sample_t); i++)
|
|
{
|
|
sum_duration += s[i].duration;
|
|
}
|
|
return sum_duration;
|
|
}
|
|
|
|
static int write_pending_data(MP4E_mux_t *mux, track_t *tr)
|
|
{
|
|
// if have pending sample && have at least one sample in the index
|
|
if (tr->pending_sample.bytes > 0 && tr->smpl.bytes >= sizeof(sample_t))
|
|
{
|
|
// Complete pending sample
|
|
sample_t *smpl_desc;
|
|
unsigned char base[8], *p = base;
|
|
|
|
assert(mux->sequential_mode_flag);
|
|
|
|
// Write each sample to a separate atom
|
|
assert(mux->sequential_mode_flag); // Separate atom needed for sequential_mode only
|
|
WRITE_4(tr->pending_sample.bytes + 8);
|
|
WRITE_4(BOX_mdat);
|
|
ERR(mux->write_callback(mux->write_pos, base, p - base, mux->token));
|
|
mux->write_pos += p - base;
|
|
|
|
// Update sample descriptor with size and offset
|
|
smpl_desc = ((sample_t*)minimp4_vector_alloc_tail(&tr->smpl, 0)) - 1;
|
|
smpl_desc->size = tr->pending_sample.bytes;
|
|
smpl_desc->offset = (boxsize_t)mux->write_pos;
|
|
|
|
// Write data
|
|
ERR(mux->write_callback(mux->write_pos, tr->pending_sample.data, tr->pending_sample.bytes, mux->token));
|
|
mux->write_pos += tr->pending_sample.bytes;
|
|
|
|
// reset buffer
|
|
tr->pending_sample.bytes = 0;
|
|
}
|
|
return MP4E_STATUS_OK;
|
|
}
|
|
|
|
static int add_sample_descriptor(MP4E_mux_t *mux, track_t *tr, int data_bytes, int duration, int kind)
|
|
{
|
|
sample_t smp;
|
|
smp.size = data_bytes;
|
|
smp.offset = (boxsize_t)mux->write_pos;
|
|
smp.duration = (duration ? duration : tr->info.default_duration);
|
|
smp.flag_random_access = (kind == MP4E_SAMPLE_RANDOM_ACCESS);
|
|
return NULL != minimp4_vector_put(&tr->smpl, &smp, sizeof(sample_t));
|
|
}
|
|
|
|
static int mp4e_flush_index(MP4E_mux_t *mux);
|
|
|
|
/**
|
|
* Write Movie Fragment: 'moof' box
|
|
*/
|
|
static int mp4e_write_fragment_header(MP4E_mux_t *mux, int track_num, int data_bytes, int duration, int kind
|
|
#if MP4D_TFDT_SUPPORT
|
|
, uint64_t timestamp
|
|
#endif
|
|
)
|
|
{
|
|
unsigned char base[888], *p = base;
|
|
unsigned char *stack_base[20]; // atoms nesting stack
|
|
unsigned char **stack = stack_base;
|
|
unsigned char *pdata_offset;
|
|
unsigned flags;
|
|
enum
|
|
{
|
|
default_sample_duration_present = 0x000008,
|
|
default_sample_flags_present = 0x000020,
|
|
} e;
|
|
|
|
track_t *tr = ((track_t*)mux->tracks.data) + track_num;
|
|
|
|
ATOM(BOX_moof)
|
|
ATOM_FULL(BOX_mfhd, 0)
|
|
WRITE_4(mux->fragments_count); // start from 1
|
|
END_ATOM
|
|
ATOM(BOX_traf)
|
|
flags = 0;
|
|
if (tr->info.track_media_kind == e_video)
|
|
flags |= 0x20; // default-sample-flags-present
|
|
else
|
|
flags |= 0x08; // default-sample-duration-present
|
|
flags = (tr->info.track_media_kind == e_video) ? 0x20020 : 0x20008;
|
|
|
|
ATOM_FULL(BOX_tfhd, flags)
|
|
WRITE_4(track_num + 1); // track_ID
|
|
if (tr->info.track_media_kind == e_video)
|
|
{
|
|
WRITE_4(0x1010000); // default_sample_flags
|
|
} else
|
|
{
|
|
WRITE_4(duration);
|
|
}
|
|
END_ATOM
|
|
#if MP4D_TFDT_SUPPORT
|
|
ATOM_FULL(BOX_tfdt, 0x01000000) // version 1
|
|
WRITE_4(timestamp >> 32); // upper timestamp
|
|
WRITE_4(timestamp & 0xffffffff); // lower timestamp
|
|
END_ATOM
|
|
#endif
|
|
if (tr->info.track_media_kind == e_audio)
|
|
{
|
|
flags = 0;
|
|
flags |= 0x001; // data-offset-present
|
|
flags |= 0x200; // sample-size-present
|
|
ATOM_FULL(BOX_trun, flags)
|
|
WRITE_4(1); // sample_count
|
|
pdata_offset = p; p += 4; // save ptr to data_offset
|
|
WRITE_4(data_bytes);// sample_size
|
|
END_ATOM
|
|
} else if (kind == MP4E_SAMPLE_RANDOM_ACCESS)
|
|
{
|
|
flags = 0;
|
|
flags |= 0x001; // data-offset-present
|
|
flags |= 0x004; // first-sample-flags-present
|
|
flags |= 0x100; // sample-duration-present
|
|
flags |= 0x200; // sample-size-present
|
|
ATOM_FULL(BOX_trun, flags)
|
|
WRITE_4(1); // sample_count
|
|
pdata_offset = p; p += 4; // save ptr to data_offset
|
|
WRITE_4(0x2000000); // first_sample_flags
|
|
WRITE_4(duration); // sample_duration
|
|
WRITE_4(data_bytes);// sample_size
|
|
END_ATOM
|
|
} else
|
|
{
|
|
flags = 0;
|
|
flags |= 0x001; // data-offset-present
|
|
flags |= 0x100; // sample-duration-present
|
|
flags |= 0x200; // sample-size-present
|
|
ATOM_FULL(BOX_trun, flags)
|
|
WRITE_4(1); // sample_count
|
|
pdata_offset = p; p += 4; // save ptr to data_offset
|
|
WRITE_4(duration); // sample_duration
|
|
WRITE_4(data_bytes);// sample_size
|
|
END_ATOM
|
|
}
|
|
END_ATOM
|
|
END_ATOM
|
|
WR4(pdata_offset, (p - base) + 8);
|
|
|
|
ERR(mux->write_callback(mux->write_pos, base, p - base, mux->token));
|
|
mux->write_pos += p - base;
|
|
return MP4E_STATUS_OK;
|
|
}
|
|
|
|
static int mp4e_write_mdat_box(MP4E_mux_t *mux, uint32_t size)
|
|
{
|
|
unsigned char base[8], *p = base;
|
|
WRITE_4(size);
|
|
WRITE_4(BOX_mdat);
|
|
ERR(mux->write_callback(mux->write_pos, base, p - base, mux->token));
|
|
mux->write_pos += p - base;
|
|
return MP4E_STATUS_OK;
|
|
}
|
|
|
|
/**
|
|
* Add new sample to specified track
|
|
*/
|
|
int MP4E_put_sample(MP4E_mux_t *mux, int track_num, const void *data, int data_bytes, int duration, int kind)
|
|
{
|
|
track_t *tr;
|
|
if (!mux || !data)
|
|
return MP4E_STATUS_BAD_ARGUMENTS;
|
|
tr = ((track_t*)mux->tracks.data) + track_num;
|
|
|
|
if (mux->enable_fragmentation)
|
|
{
|
|
#if MP4D_TFDT_SUPPORT
|
|
// NOTE: assume a constant `duration` to calculate current timestamp
|
|
uint64_t timestamp = (uint64_t)mux->fragments_count * duration;
|
|
#endif
|
|
if (!mux->fragments_count++)
|
|
ERR(mp4e_flush_index(mux)); // write file headers before 1st sample
|
|
// write MOOF + MDAT + sample data
|
|
#if MP4D_TFDT_SUPPORT
|
|
ERR(mp4e_write_fragment_header(mux, track_num, data_bytes, duration, kind, timestamp));
|
|
#else
|
|
ERR(mp4e_write_fragment_header(mux, track_num, data_bytes, duration, kind));
|
|
#endif
|
|
// write MDAT box for each sample
|
|
ERR(mp4e_write_mdat_box(mux, data_bytes + 8));
|
|
ERR(mux->write_callback(mux->write_pos, data, data_bytes, mux->token));
|
|
mux->write_pos += data_bytes;
|
|
return MP4E_STATUS_OK;
|
|
}
|
|
|
|
if (kind != MP4E_SAMPLE_CONTINUATION)
|
|
{
|
|
if (mux->sequential_mode_flag)
|
|
ERR(write_pending_data(mux, tr));
|
|
if (!add_sample_descriptor(mux, tr, data_bytes, duration, kind))
|
|
return MP4E_STATUS_NO_MEMORY;
|
|
} else
|
|
{
|
|
if (!mux->sequential_mode_flag)
|
|
{
|
|
sample_t *smpl_desc;
|
|
if (tr->smpl.bytes < sizeof(sample_t))
|
|
return MP4E_STATUS_NO_MEMORY; // write continuation, but there are no samples in the index
|
|
// Accumulate size of the continuation in the sample descriptor
|
|
smpl_desc = (sample_t*)(tr->smpl.data + tr->smpl.bytes) - 1;
|
|
smpl_desc->size += data_bytes;
|
|
}
|
|
}
|
|
|
|
if (mux->sequential_mode_flag)
|
|
{
|
|
if (!minimp4_vector_put(&tr->pending_sample, data, data_bytes))
|
|
return MP4E_STATUS_NO_MEMORY;
|
|
} else
|
|
{
|
|
ERR(mux->write_callback(mux->write_pos, data, data_bytes, mux->token));
|
|
mux->write_pos += data_bytes;
|
|
}
|
|
return MP4E_STATUS_OK;
|
|
}
|
|
|
|
/**
|
|
* calculate size of length field of OD box
|
|
*/
|
|
static int od_size_of_size(int size)
|
|
{
|
|
int i, size_of_size = 1;
|
|
for (i = size; i > 0x7F; i -= 0x7F)
|
|
size_of_size++;
|
|
return size_of_size;
|
|
}
|
|
|
|
/**
|
|
* Add or remove MP4 file text comment according to Apple specs:
|
|
* https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW1
|
|
* http://atomicparsley.sourceforge.net/mpeg-4files.html
|
|
* note that ISO did not specify comment format.
|
|
*/
|
|
int MP4E_set_text_comment(MP4E_mux_t *mux, const char *comment)
|
|
{
|
|
if (!mux || !comment)
|
|
return MP4E_STATUS_BAD_ARGUMENTS;
|
|
if (mux->text_comment)
|
|
free(mux->text_comment);
|
|
mux->text_comment = strdup(comment);
|
|
if (!mux->text_comment)
|
|
return MP4E_STATUS_NO_MEMORY;
|
|
return MP4E_STATUS_OK;
|
|
}
|
|
|
|
/**
|
|
* Write file index 'moov' box with all its boxes and indexes
|
|
*/
|
|
static int mp4e_flush_index(MP4E_mux_t *mux)
|
|
{
|
|
unsigned char *stack_base[20]; // atoms nesting stack
|
|
unsigned char **stack = stack_base;
|
|
unsigned char *base, *p;
|
|
unsigned int ntr, index_bytes, ntracks = mux->tracks.bytes / sizeof(track_t);
|
|
int i, err;
|
|
|
|
// How much memory needed for indexes
|
|
// Experimental data:
|
|
// file with 1 track = 560 bytes
|
|
// file with 2 tracks = 972 bytes
|
|
// track size = 412 bytes;
|
|
// file header size = 148 bytes
|
|
#define FILE_HEADER_BYTES 256
|
|
#define TRACK_HEADER_BYTES 512
|
|
index_bytes = FILE_HEADER_BYTES;
|
|
if (mux->text_comment)
|
|
index_bytes += 128 + strlen(mux->text_comment);
|
|
for (ntr = 0; ntr < ntracks; ntr++)
|
|
{
|
|
track_t *tr = ((track_t*)mux->tracks.data) + ntr;
|
|
index_bytes += TRACK_HEADER_BYTES; // fixed amount (implementation-dependent)
|
|
// may need extra 4 bytes for duration field + 4 bytes for worst-case random access box
|
|
index_bytes += tr->smpl.bytes * (sizeof(sample_t) + 4 + 4) / sizeof(sample_t);
|
|
index_bytes += tr->vsps.bytes;
|
|
index_bytes += tr->vpps.bytes;
|
|
|
|
ERR(write_pending_data(mux, tr));
|
|
}
|
|
|
|
base = (unsigned char*)malloc(index_bytes);
|
|
if (!base)
|
|
return MP4E_STATUS_NO_MEMORY;
|
|
p = base;
|
|
|
|
if (!mux->sequential_mode_flag)
|
|
{
|
|
// update size of mdat box.
|
|
// One of 2 points, which requires random file access.
|
|
// Second is optional duration update at beginning of file in fragmentation mode.
|
|
// This can be avoided using "till eof" size code, but in this case indexes must be
|
|
// written before the mdat....
|
|
int64_t size = mux->write_pos - sizeof(box_ftyp);
|
|
const int64_t size_limit = (int64_t)(uint64_t)0xfffffffe;
|
|
if (size > size_limit)
|
|
{
|
|
WRITE_4(1);
|
|
WRITE_4(BOX_mdat);
|
|
WRITE_4((size >> 32) & 0xffffffff);
|
|
WRITE_4(size & 0xffffffff);
|
|
} else
|
|
{
|
|
WRITE_4(8);
|
|
WRITE_4(BOX_free);
|
|
WRITE_4(size - 8);
|
|
WRITE_4(BOX_mdat);
|
|
}
|
|
ERR(mux->write_callback(sizeof(box_ftyp), base, p - base, mux->token));
|
|
p = base;
|
|
}
|
|
|
|
// Write index atoms; order taken from Table 1 of [1]
|
|
#define MOOV_TIMESCALE 1000
|
|
ATOM(BOX_moov);
|
|
ATOM_FULL(BOX_mvhd, 0);
|
|
WRITE_4(0); // creation_time
|
|
WRITE_4(0); // modification_time
|
|
|
|
if (ntracks)
|
|
{
|
|
track_t *tr = ((track_t*)mux->tracks.data) + 0; // take 1st track
|
|
unsigned duration = get_duration(tr);
|
|
duration = (unsigned)(duration * 1LL * MOOV_TIMESCALE / tr->info.time_scale);
|
|
WRITE_4(MOOV_TIMESCALE); // duration
|
|
WRITE_4(duration); // duration
|
|
}
|
|
|
|
WRITE_4(0x00010000); // rate
|
|
WRITE_2(0x0100); // volume
|
|
WRITE_2(0); // reserved
|
|
WRITE_4(0); // reserved
|
|
WRITE_4(0); // reserved
|
|
|
|
// matrix[9]
|
|
WRITE_4(0x00010000); WRITE_4(0); WRITE_4(0);
|
|
WRITE_4(0); WRITE_4(0x00010000); WRITE_4(0);
|
|
WRITE_4(0); WRITE_4(0); WRITE_4(0x40000000);
|
|
|
|
// pre_defined[6]
|
|
WRITE_4(0); WRITE_4(0); WRITE_4(0);
|
|
WRITE_4(0); WRITE_4(0); WRITE_4(0);
|
|
|
|
//next_track_ID is a non-zero integer that indicates a value to use for the track ID of the next track to be
|
|
//added to this presentation. Zero is not a valid track ID value. The value of next_track_ID shall be
|
|
//larger than the largest track-ID in use.
|
|
WRITE_4(ntracks + 1);
|
|
END_ATOM;
|
|
|
|
for (ntr = 0; ntr < ntracks; ntr++)
|
|
{
|
|
track_t *tr = ((track_t*)mux->tracks.data) + ntr;
|
|
unsigned duration = get_duration(tr);
|
|
int samples_count = tr->smpl.bytes / sizeof(sample_t);
|
|
const sample_t *sample = (const sample_t *)tr->smpl.data;
|
|
unsigned handler_type;
|
|
const char *handler_ascii = NULL;
|
|
|
|
if (mux->enable_fragmentation)
|
|
samples_count = 0;
|
|
else if (samples_count <= 0)
|
|
continue; // skip empty track
|
|
|
|
switch (tr->info.track_media_kind)
|
|
{
|
|
case e_audio:
|
|
handler_type = MP4E_HANDLER_TYPE_SOUN;
|
|
handler_ascii = "SoundHandler";
|
|
break;
|
|
case e_video:
|
|
handler_type = MP4E_HANDLER_TYPE_VIDE;
|
|
handler_ascii = "VideoHandler";
|
|
break;
|
|
case e_private:
|
|
handler_type = MP4E_HANDLER_TYPE_GESM;
|
|
break;
|
|
default:
|
|
return MP4E_STATUS_BAD_ARGUMENTS;
|
|
}
|
|
|
|
ATOM(BOX_trak);
|
|
ATOM_FULL(BOX_tkhd, 7); // flag: 1=trak enabled; 2=track in movie; 4=track in preview
|
|
WRITE_4(0); // creation_time
|
|
WRITE_4(0); // modification_time
|
|
WRITE_4(ntr + 1); // track_ID
|
|
WRITE_4(0); // reserved
|
|
WRITE_4((unsigned)(duration * 1LL * MOOV_TIMESCALE / tr->info.time_scale));
|
|
WRITE_4(0); WRITE_4(0); // reserved[2]
|
|
WRITE_2(0); // layer
|
|
WRITE_2(0); // alternate_group
|
|
WRITE_2(0x0100); // volume {if track_is_audio 0x0100 else 0};
|
|
WRITE_2(0); // reserved
|
|
|
|
// matrix[9]
|
|
WRITE_4(0x00010000); WRITE_4(0); WRITE_4(0);
|
|
WRITE_4(0); WRITE_4(0x00010000); WRITE_4(0);
|
|
WRITE_4(0); WRITE_4(0); WRITE_4(0x40000000);
|
|
|
|
if (tr->info.track_media_kind == e_audio || tr->info.track_media_kind == e_private)
|
|
{
|
|
WRITE_4(0); // width
|
|
WRITE_4(0); // height
|
|
} else
|
|
{
|
|
WRITE_4(tr->info.u.v.width*0x10000); // width
|
|
WRITE_4(tr->info.u.v.height*0x10000); // height
|
|
}
|
|
END_ATOM;
|
|
|
|
ATOM(BOX_mdia);
|
|
ATOM_FULL(BOX_mdhd, 0);
|
|
WRITE_4(0); // creation_time
|
|
WRITE_4(0); // modification_time
|
|
WRITE_4(tr->info.time_scale);
|
|
WRITE_4(duration); // duration
|
|
{
|
|
int lang_code = ((tr->info.language[0] & 31) << 10) | ((tr->info.language[1] & 31) << 5) | (tr->info.language[2] & 31);
|
|
WRITE_2(lang_code); // language
|
|
}
|
|
WRITE_2(0); // pre_defined
|
|
END_ATOM;
|
|
|
|
ATOM_FULL(BOX_hdlr, 0);
|
|
WRITE_4(0); // pre_defined
|
|
WRITE_4(handler_type); // handler_type
|
|
WRITE_4(0); WRITE_4(0); WRITE_4(0); // reserved[3]
|
|
// name is a null-terminated string in UTF-8 characters which gives a human-readable name for the track type (for debugging and inspection purposes).
|
|
// set mdia hdlr name field to what quicktime uses.
|
|
// Sony smartphone may fail to decode short files w/o handler name
|
|
if (handler_ascii)
|
|
{
|
|
for (i = 0; i < (int)strlen(handler_ascii) + 1; i++)
|
|
{
|
|
WRITE_1(handler_ascii[i]);
|
|
}
|
|
} else
|
|
{
|
|
WRITE_4(0);
|
|
}
|
|
END_ATOM;
|
|
|
|
ATOM(BOX_minf);
|
|
|
|
if (tr->info.track_media_kind == e_audio)
|
|
{
|
|
// Sound Media Header Box
|
|
ATOM_FULL(BOX_smhd, 0);
|
|
WRITE_2(0); // balance
|
|
WRITE_2(0); // reserved
|
|
END_ATOM;
|
|
}
|
|
if (tr->info.track_media_kind == e_video)
|
|
{
|
|
// mandatory Video Media Header Box
|
|
ATOM_FULL(BOX_vmhd, 1);
|
|
WRITE_2(0); // graphicsmode
|
|
WRITE_2(0); WRITE_2(0); WRITE_2(0); // opcolor[3]
|
|
END_ATOM;
|
|
}
|
|
|
|
ATOM(BOX_dinf);
|
|
ATOM_FULL(BOX_dref, 0);
|
|
WRITE_4(1); // entry_count
|
|
// If the flag is set indicating that the data is in the same file as this box, then no string (not even an empty one)
|
|
// shall be supplied in the entry field.
|
|
|
|
// ASP the correct way to avoid supply the string, is to use flag 1
|
|
// otherwise ISO reference demux crashes
|
|
ATOM_FULL(BOX_url, 1);
|
|
END_ATOM;
|
|
END_ATOM;
|
|
END_ATOM;
|
|
|
|
ATOM(BOX_stbl);
|
|
ATOM_FULL(BOX_stsd, 0);
|
|
WRITE_4(1); // entry_count;
|
|
|
|
if (tr->info.track_media_kind == e_audio || tr->info.track_media_kind == e_private)
|
|
{
|
|
// AudioSampleEntry() assume MP4E_HANDLER_TYPE_SOUN
|
|
if (tr->info.track_media_kind == e_audio)
|
|
{
|
|
ATOM(BOX_mp4a);
|
|
} else
|
|
{
|
|
ATOM(BOX_mp4s);
|
|
}
|
|
|
|
// SampleEntry
|
|
WRITE_4(0); WRITE_2(0); // reserved[6]
|
|
WRITE_2(1); // data_reference_index; - this is a tag for descriptor below
|
|
|
|
if (tr->info.track_media_kind == e_audio)
|
|
{
|
|
// AudioSampleEntry
|
|
WRITE_4(0); WRITE_4(0); // reserved[2]
|
|
WRITE_2(tr->info.u.a.channelcount); // channelcount
|
|
WRITE_2(16); // samplesize
|
|
WRITE_4(0); // pre_defined+reserved
|
|
WRITE_4((tr->info.time_scale << 16)); // samplerate == = {timescale of media}<<16;
|
|
}
|
|
|
|
ATOM_FULL(BOX_esds, 0);
|
|
if (tr->vsps.bytes > 0)
|
|
{
|
|
int dsi_bytes = tr->vsps.bytes - 2; // - two bytes size field
|
|
int dsi_size_size = od_size_of_size(dsi_bytes);
|
|
int dcd_bytes = dsi_bytes + dsi_size_size + 1 + (1 + 1 + 3 + 4 + 4);
|
|
int dcd_size_size = od_size_of_size(dcd_bytes);
|
|
int esd_bytes = dcd_bytes + dcd_size_size + 1 + 3;
|
|
|
|
#define WRITE_OD_LEN(size) if (size > 0x7F) do { size -= 0x7F; WRITE_1(0x00ff); } while (size > 0x7F); WRITE_1(size)
|
|
WRITE_1(3); // OD_ESD
|
|
WRITE_OD_LEN(esd_bytes);
|
|
WRITE_2(0); // ES_ID(2) // TODO - what is this?
|
|
WRITE_1(0); // flags(1)
|
|
|
|
WRITE_1(4); // OD_DCD
|
|
WRITE_OD_LEN(dcd_bytes);
|
|
if (tr->info.track_media_kind == e_audio)
|
|
{
|
|
WRITE_1(MP4_OBJECT_TYPE_AUDIO_ISO_IEC_14496_3); // OD_DCD
|
|
WRITE_1(5 << 2); // stream_type == AudioStream
|
|
} else
|
|
{
|
|
// http://xhelmboyx.tripod.com/formats/mp4-layout.txt
|
|
WRITE_1(208); // 208 = private video
|
|
WRITE_1(32 << 2); // stream_type == user private
|
|
}
|
|
WRITE_3(tr->info.u.a.channelcount * 6144/8); // bufferSizeDB in bytes, constant as in reference decoder
|
|
WRITE_4(0); // maxBitrate TODO
|
|
WRITE_4(0); // avg_bitrate_bps TODO
|
|
|
|
WRITE_1(5); // OD_DSI
|
|
WRITE_OD_LEN(dsi_bytes);
|
|
for (i = 0; i < dsi_bytes; i++)
|
|
{
|
|
WRITE_1(tr->vsps.data[2 + i]);
|
|
}
|
|
}
|
|
END_ATOM;
|
|
END_ATOM;
|
|
}
|
|
|
|
if (tr->info.track_media_kind == e_video && (MP4_OBJECT_TYPE_AVC == tr->info.object_type_indication || MP4_OBJECT_TYPE_HEVC == tr->info.object_type_indication))
|
|
{
|
|
int numOfSequenceParameterSets = items_count(&tr->vsps);
|
|
int numOfPictureParameterSets = items_count(&tr->vpps);
|
|
if (MP4_OBJECT_TYPE_AVC == tr->info.object_type_indication)
|
|
{
|
|
ATOM(BOX_avc1);
|
|
} else
|
|
{
|
|
ATOM(BOX_hvc1);
|
|
}
|
|
// VisualSampleEntry 8.16.2
|
|
// extends SampleEntry
|
|
WRITE_2(0); // reserved
|
|
WRITE_2(0); // reserved
|
|
WRITE_2(0); // reserved
|
|
WRITE_2(1); // data_reference_index
|
|
|
|
WRITE_2(0); // pre_defined
|
|
WRITE_2(0); // reserved
|
|
WRITE_4(0); // pre_defined
|
|
WRITE_4(0); // pre_defined
|
|
WRITE_4(0); // pre_defined
|
|
WRITE_2(tr->info.u.v.width);
|
|
WRITE_2(tr->info.u.v.height);
|
|
WRITE_4(0x00480000); // horizresolution = 72 dpi
|
|
WRITE_4(0x00480000); // vertresolution = 72 dpi
|
|
WRITE_4(0); // reserved
|
|
WRITE_2(1); // frame_count
|
|
for (i = 0; i < 32; i++)
|
|
{
|
|
WRITE_1(0); // compressorname
|
|
}
|
|
WRITE_2(24); // depth
|
|
WRITE_2(-1); // pre_defined
|
|
|
|
if (MP4_OBJECT_TYPE_AVC == tr->info.object_type_indication)
|
|
{
|
|
ATOM(BOX_avcC);
|
|
// AVCDecoderConfigurationRecord 5.2.4.1.1
|
|
WRITE_1(1); // configurationVersion
|
|
WRITE_1(tr->vsps.data[2 + 1]);
|
|
WRITE_1(tr->vsps.data[2 + 2]);
|
|
WRITE_1(tr->vsps.data[2 + 3]);
|
|
WRITE_1(255); // 0xfc + NALU_len - 1
|
|
WRITE_1(0xe0 | numOfSequenceParameterSets);
|
|
for (i = 0; i < tr->vsps.bytes; i++)
|
|
{
|
|
WRITE_1(tr->vsps.data[i]);
|
|
}
|
|
WRITE_1(numOfPictureParameterSets);
|
|
for (i = 0; i < tr->vpps.bytes; i++)
|
|
{
|
|
WRITE_1(tr->vpps.data[i]);
|
|
}
|
|
} else
|
|
{
|
|
int numOfVPS = items_count(&tr->vpps);
|
|
ATOM(BOX_hvcC);
|
|
// TODO: read actual params from stream
|
|
WRITE_1(1); // configurationVersion
|
|
WRITE_1(1); // Profile Space (2), Tier (1), Profile (5)
|
|
WRITE_4(0x60000000); // Profile Compatibility
|
|
WRITE_2(0); // progressive, interlaced, non packed constraint, frame only constraint flags
|
|
WRITE_4(0); // constraint indicator flags
|
|
WRITE_1(0); // level_idc
|
|
WRITE_2(0xf000); // Min Spatial Segmentation
|
|
WRITE_1(0xfc); // Parallelism Type
|
|
WRITE_1(0xfc); // Chroma Format
|
|
WRITE_1(0xf8); // Luma Depth
|
|
WRITE_1(0xf8); // Chroma Depth
|
|
WRITE_2(0); // Avg Frame Rate
|
|
WRITE_1(3); // ConstantFrameRate (2), NumTemporalLayers (3), TemporalIdNested (1), LengthSizeMinusOne (2)
|
|
|
|
WRITE_1(3); // Num Of Arrays
|
|
WRITE_1((1 << 7) | (HEVC_NAL_VPS & 0x3f)); // Array Completeness + NAL Unit Type
|
|
WRITE_2(numOfVPS);
|
|
for (i = 0; i < tr->vvps.bytes; i++)
|
|
{
|
|
WRITE_1(tr->vvps.data[i]);
|
|
}
|
|
WRITE_1((1 << 7) | (HEVC_NAL_SPS & 0x3f));
|
|
WRITE_2(numOfSequenceParameterSets);
|
|
for (i = 0; i < tr->vsps.bytes; i++)
|
|
{
|
|
WRITE_1(tr->vsps.data[i]);
|
|
}
|
|
WRITE_1((1 << 7) | (HEVC_NAL_PPS & 0x3f));
|
|
WRITE_2(numOfPictureParameterSets);
|
|
for (i = 0; i < tr->vpps.bytes; i++)
|
|
{
|
|
WRITE_1(tr->vpps.data[i]);
|
|
}
|
|
}
|
|
|
|
END_ATOM;
|
|
END_ATOM;
|
|
}
|
|
END_ATOM;
|
|
|
|
/************************************************************************/
|
|
/* indexes */
|
|
/************************************************************************/
|
|
|
|
// Time to Sample Box
|
|
ATOM_FULL(BOX_stts, 0);
|
|
{
|
|
unsigned char *pentry_count = p;
|
|
int cnt = 1, entry_count = 0;
|
|
WRITE_4(0);
|
|
for (i = 0; i < samples_count; i++, cnt++)
|
|
{
|
|
if (i == (samples_count - 1) || sample[i].duration != sample[i + 1].duration)
|
|
{
|
|
WRITE_4(cnt);
|
|
WRITE_4(sample[i].duration);
|
|
cnt = 0;
|
|
entry_count++;
|
|
}
|
|
}
|
|
WR4(pentry_count, entry_count);
|
|
}
|
|
END_ATOM;
|
|
|
|
// Sample To Chunk Box
|
|
ATOM_FULL(BOX_stsc, 0);
|
|
if (mux->enable_fragmentation)
|
|
{
|
|
WRITE_4(0); // entry_count
|
|
} else
|
|
{
|
|
WRITE_4(1); // entry_count
|
|
WRITE_4(1); // first_chunk;
|
|
WRITE_4(1); // samples_per_chunk;
|
|
WRITE_4(1); // sample_description_index;
|
|
}
|
|
END_ATOM;
|
|
|
|
// Sample Size Box
|
|
ATOM_FULL(BOX_stsz, 0);
|
|
WRITE_4(0); // sample_size If this field is set to 0, then the samples have different sizes, and those sizes
|
|
// are stored in the sample size table.
|
|
WRITE_4(samples_count); // sample_count;
|
|
for (i = 0; i < samples_count; i++)
|
|
{
|
|
WRITE_4(sample[i].size);
|
|
}
|
|
END_ATOM;
|
|
|
|
// Chunk Offset Box
|
|
int is_64_bit = 0;
|
|
if (samples_count && sample[samples_count - 1].offset > 0xffffffff)
|
|
is_64_bit = 1;
|
|
if (!is_64_bit)
|
|
{
|
|
ATOM_FULL(BOX_stco, 0);
|
|
WRITE_4(samples_count);
|
|
for (i = 0; i < samples_count; i++)
|
|
{
|
|
WRITE_4(sample[i].offset);
|
|
}
|
|
} else
|
|
{
|
|
ATOM_FULL(BOX_co64, 0);
|
|
WRITE_4(samples_count);
|
|
for (i = 0; i < samples_count; i++)
|
|
{
|
|
WRITE_4((sample[i].offset >> 32) & 0xffffffff);
|
|
WRITE_4(sample[i].offset & 0xffffffff);
|
|
}
|
|
}
|
|
END_ATOM;
|
|
|
|
// Sync Sample Box
|
|
{
|
|
int ra_count = 0;
|
|
for (i = 0; i < samples_count; i++)
|
|
{
|
|
ra_count += !!sample[i].flag_random_access;
|
|
}
|
|
if (ra_count != samples_count)
|
|
{
|
|
// If the sync sample box is not present, every sample is a random access point.
|
|
ATOM_FULL(BOX_stss, 0);
|
|
WRITE_4(ra_count);
|
|
for (i = 0; i < samples_count; i++)
|
|
{
|
|
if (sample[i].flag_random_access)
|
|
{
|
|
WRITE_4(i + 1);
|
|
}
|
|
}
|
|
END_ATOM;
|
|
}
|
|
}
|
|
END_ATOM;
|
|
END_ATOM;
|
|
END_ATOM;
|
|
END_ATOM;
|
|
} // tracks loop
|
|
|
|
if (mux->text_comment)
|
|
{
|
|
ATOM(BOX_udta);
|
|
ATOM_FULL(BOX_meta, 0);
|
|
ATOM_FULL(BOX_hdlr, 0);
|
|
WRITE_4(0); // pre_defined
|
|
#define MP4E_HANDLER_TYPE_MDIR 0x6d646972
|
|
WRITE_4(MP4E_HANDLER_TYPE_MDIR); // handler_type
|
|
WRITE_4(0); WRITE_4(0); WRITE_4(0); // reserved[3]
|
|
WRITE_4(0); // name is a null-terminated string in UTF-8 characters which gives a human-readable name for the track type (for debugging and inspection purposes).
|
|
END_ATOM;
|
|
ATOM(BOX_ilst);
|
|
ATOM(BOX_ccmt);
|
|
ATOM(BOX_data);
|
|
WRITE_4(1); // type
|
|
WRITE_4(0); // lang
|
|
for (i = 0; i < (int)strlen(mux->text_comment) + 1; i++)
|
|
{
|
|
WRITE_1(mux->text_comment[i]);
|
|
}
|
|
END_ATOM;
|
|
END_ATOM;
|
|
END_ATOM;
|
|
END_ATOM;
|
|
END_ATOM;
|
|
}
|
|
|
|
if (mux->enable_fragmentation)
|
|
{
|
|
track_t *tr = ((track_t*)mux->tracks.data) + 0;
|
|
uint32_t movie_duration = get_duration(tr);
|
|
|
|
ATOM(BOX_mvex);
|
|
ATOM_FULL(BOX_mehd, 0);
|
|
WRITE_4(movie_duration); // duration
|
|
END_ATOM;
|
|
for (ntr = 0; ntr < ntracks; ntr++)
|
|
{
|
|
ATOM_FULL(BOX_trex, 0);
|
|
WRITE_4(ntr + 1); // track_ID
|
|
WRITE_4(1); // default_sample_description_index
|
|
WRITE_4(0); // default_sample_duration
|
|
WRITE_4(0); // default_sample_size
|
|
WRITE_4(0); // default_sample_flags
|
|
END_ATOM;
|
|
}
|
|
END_ATOM;
|
|
}
|
|
END_ATOM; // moov atom
|
|
|
|
assert((unsigned)(p - base) <= index_bytes);
|
|
|
|
err = mux->write_callback(mux->write_pos, base, p - base, mux->token);
|
|
mux->write_pos += p - base;
|
|
free(base);
|
|
return err;
|
|
}
|
|
|
|
int MP4E_close(MP4E_mux_t *mux)
|
|
{
|
|
int err = MP4E_STATUS_OK;
|
|
unsigned ntr, ntracks;
|
|
if (!mux)
|
|
return MP4E_STATUS_BAD_ARGUMENTS;
|
|
if (!mux->enable_fragmentation)
|
|
err = mp4e_flush_index(mux);
|
|
if (mux->text_comment)
|
|
free(mux->text_comment);
|
|
ntracks = mux->tracks.bytes / sizeof(track_t);
|
|
for (ntr = 0; ntr < ntracks; ntr++)
|
|
{
|
|
track_t *tr = ((track_t*)mux->tracks.data) + ntr;
|
|
minimp4_vector_reset(&tr->vsps);
|
|
minimp4_vector_reset(&tr->vpps);
|
|
minimp4_vector_reset(&tr->smpl);
|
|
minimp4_vector_reset(&tr->pending_sample);
|
|
}
|
|
minimp4_vector_reset(&mux->tracks);
|
|
free(mux);
|
|
return err;
|
|
}
|
|
|
|
typedef uint32_t bs_item_t;
|
|
#define BS_BITS 32
|
|
|
|
typedef struct
|
|
{
|
|
// Look-ahead bit cache: MSB aligned, 17 bits guaranteed, zero stuffing
|
|
unsigned int cache;
|
|
|
|
// Bit counter = 16 - (number of bits in wCache)
|
|
// cache refilled when cache_free_bits >= 0
|
|
int cache_free_bits;
|
|
|
|
// Current read position
|
|
const uint16_t *buf;
|
|
|
|
// original data buffer
|
|
const uint16_t *origin;
|
|
|
|
// original data buffer length, bytes
|
|
unsigned origin_bytes;
|
|
} bit_reader_t;
|
|
|
|
|
|
#define LOAD_SHORT(x) ((uint16_t)(x << 8) | (x >> 8))
|
|
|
|
static unsigned int show_bits(bit_reader_t *bs, int n)
|
|
{
|
|
unsigned int retval;
|
|
assert(n > 0 && n <= 16);
|
|
retval = (unsigned int)(bs->cache >> (32 - n));
|
|
return retval;
|
|
}
|
|
|
|
static void flush_bits(bit_reader_t *bs, int n)
|
|
{
|
|
assert(n >= 0 && n <= 16);
|
|
bs->cache <<= n;
|
|
bs->cache_free_bits += n;
|
|
if (bs->cache_free_bits >= 0)
|
|
{
|
|
bs->cache |= ((uint32_t)LOAD_SHORT(*bs->buf)) << bs->cache_free_bits;
|
|
bs->buf++;
|
|
bs->cache_free_bits -= 16;
|
|
}
|
|
}
|
|
|
|
static unsigned int get_bits(bit_reader_t *bs, int n)
|
|
{
|
|
unsigned int retval = show_bits(bs, n);
|
|
flush_bits(bs, n);
|
|
return retval;
|
|
}
|
|
|
|
static void set_pos_bits(bit_reader_t *bs, unsigned pos_bits)
|
|
{
|
|
assert((int)pos_bits >= 0);
|
|
|
|
bs->buf = bs->origin + pos_bits/16;
|
|
bs->cache = 0;
|
|
bs->cache_free_bits = 16;
|
|
flush_bits(bs, 0);
|
|
flush_bits(bs, pos_bits & 15);
|
|
}
|
|
|
|
static unsigned get_pos_bits(const bit_reader_t *bs)
|
|
{
|
|
// Current bitbuffer position =
|
|
// position of next wobits in the internal buffer
|
|
// minus bs, available in bit cache wobits
|
|
unsigned pos_bits = (unsigned)(bs->buf - bs->origin)*16;
|
|
pos_bits -= 16 - bs->cache_free_bits;
|
|
assert((int)pos_bits >= 0);
|
|
return pos_bits;
|
|
}
|
|
|
|
static int remaining_bits(const bit_reader_t *bs)
|
|
{
|
|
return bs->origin_bytes * 8 - get_pos_bits(bs);
|
|
}
|
|
|
|
static void init_bits(bit_reader_t *bs, const void *data, unsigned data_bytes)
|
|
{
|
|
bs->origin = (const uint16_t *)data;
|
|
bs->origin_bytes = data_bytes;
|
|
set_pos_bits(bs, 0);
|
|
}
|
|
|
|
#define GetBits(n) get_bits(bs, n)
|
|
|
|
/**
|
|
* Unsigned Golomb code
|
|
*/
|
|
static int ue_bits(bit_reader_t *bs)
|
|
{
|
|
int clz;
|
|
int val;
|
|
for (clz = 0; !get_bits(bs, 1); clz++) {}
|
|
//get_bits(bs, clz + 1);
|
|
val = (1 << clz) - 1 + (clz ? get_bits(bs, clz) : 0);
|
|
return val;
|
|
}
|
|
|
|
#if MINIMP4_TRANSCODE_SPS_ID
|
|
|
|
/**
|
|
* Output bitstream
|
|
*/
|
|
typedef struct
|
|
{
|
|
int shift; // bit position in the cache
|
|
uint32_t cache; // bit cache
|
|
bs_item_t *buf; // current position
|
|
bs_item_t *origin; // initial position
|
|
} bs_t;
|
|
|
|
#define SWAP32(x) (uint32_t)((((x) >> 24) & 0xFF) | (((x) >> 8) & 0xFF00) | (((x) << 8) & 0xFF0000) | ((x & 0xFF) << 24))
|
|
|
|
static void h264e_bs_put_bits(bs_t *bs, unsigned n, unsigned val)
|
|
{
|
|
assert(!(val >> n));
|
|
bs->shift -= n;
|
|
assert((unsigned)n <= 32);
|
|
if (bs->shift < 0)
|
|
{
|
|
assert(-bs->shift < 32);
|
|
bs->cache |= val >> -bs->shift;
|
|
*bs->buf++ = SWAP32(bs->cache);
|
|
bs->shift = 32 + bs->shift;
|
|
bs->cache = 0;
|
|
}
|
|
bs->cache |= val << bs->shift;
|
|
}
|
|
|
|
static void h264e_bs_flush(bs_t *bs)
|
|
{
|
|
*bs->buf = SWAP32(bs->cache);
|
|
}
|
|
|
|
static unsigned h264e_bs_get_pos_bits(const bs_t *bs)
|
|
{
|
|
unsigned pos_bits = (unsigned)((bs->buf - bs->origin)*BS_BITS);
|
|
pos_bits += BS_BITS - bs->shift;
|
|
assert((int)pos_bits >= 0);
|
|
return pos_bits;
|
|
}
|
|
|
|
static unsigned h264e_bs_byte_align(bs_t *bs)
|
|
{
|
|
int pos = h264e_bs_get_pos_bits(bs);
|
|
h264e_bs_put_bits(bs, -pos & 7, 0);
|
|
return pos + (-pos & 7);
|
|
}
|
|
|
|
/**
|
|
* Golomb code
|
|
* 0 => 1
|
|
* 1 => 01 0
|
|
* 2 => 01 1
|
|
* 3 => 001 00
|
|
* 4 => 001 01
|
|
*
|
|
* [0] => 1
|
|
* [1..2] => 01x
|
|
* [3..6] => 001xx
|
|
* [7..14] => 0001xxx
|
|
*
|
|
*/
|
|
static void h264e_bs_put_golomb(bs_t *bs, unsigned val)
|
|
{
|
|
int size = 0;
|
|
unsigned t = val + 1;
|
|
do
|
|
{
|
|
size++;
|
|
} while (t >>= 1);
|
|
|
|
h264e_bs_put_bits(bs, 2*size - 1, val + 1);
|
|
}
|
|
|
|
static void h264e_bs_init_bits(bs_t *bs, void *data)
|
|
{
|
|
bs->origin = (bs_item_t*)data;
|
|
bs->buf = bs->origin;
|
|
bs->shift = BS_BITS;
|
|
bs->cache = 0;
|
|
}
|
|
|
|
static int find_mem_cache(void *cache[], int cache_bytes[], int cache_size, void *mem, int bytes)
|
|
{
|
|
int i;
|
|
if (!bytes)
|
|
return -1;
|
|
for (i = 0; i < cache_size; i++)
|
|
{
|
|
if (cache_bytes[i] == bytes && !memcmp(mem, cache[i], bytes))
|
|
return i; // found
|
|
}
|
|
for (i = 0; i < cache_size; i++)
|
|
{
|
|
if (!cache_bytes[i])
|
|
{
|
|
cache[i] = malloc(bytes);
|
|
if (cache[i])
|
|
{
|
|
memcpy(cache[i], mem, bytes);
|
|
cache_bytes[i] = bytes;
|
|
}
|
|
return i; // put in
|
|
}
|
|
}
|
|
return -1; // no room
|
|
}
|
|
|
|
/**
|
|
* 7.4.1.1. "Encapsulation of an SODB within an RBSP"
|
|
*/
|
|
static int remove_nal_escapes(unsigned char *dst, const unsigned char *src, int h264_data_bytes)
|
|
{
|
|
int i = 0, j = 0, zero_cnt = 0;
|
|
for (j = 0; j < h264_data_bytes; j++)
|
|
{
|
|
if (zero_cnt == 2 && src[j] <= 3)
|
|
{
|
|
if (src[j] == 3)
|
|
{
|
|
if (j == h264_data_bytes - 1)
|
|
{
|
|
// cabac_zero_word: no action
|
|
} else if (src[j + 1] <= 3)
|
|
{
|
|
j++;
|
|
zero_cnt = 0;
|
|
} else
|
|
{
|
|
// TODO: assume end-of-nal
|
|
//return 0;
|
|
}
|
|
} else
|
|
return 0;
|
|
}
|
|
dst[i++] = src[j];
|
|
if (src[j])
|
|
zero_cnt = 0;
|
|
else
|
|
zero_cnt++;
|
|
}
|
|
//while (--j > i) src[j] = 0;
|
|
return i;
|
|
}
|
|
|
|
/**
|
|
* Put NAL escape codes to the output bitstream
|
|
*/
|
|
static int nal_put_esc(uint8_t *d, const uint8_t *s, int n)
|
|
{
|
|
int i, j = 4, cntz = 0;
|
|
d[0] = d[1] = d[2] = 0; d[3] = 1; // start code
|
|
for (i = 0; i < n; i++)
|
|
{
|
|
uint8_t byte = *s++;
|
|
if (cntz == 2 && byte <= 3)
|
|
{
|
|
d[j++] = 3;
|
|
cntz = 0;
|
|
}
|
|
if (byte)
|
|
cntz = 0;
|
|
else
|
|
cntz++;
|
|
d[j++] = byte;
|
|
}
|
|
return j;
|
|
}
|
|
|
|
static void copy_bits(bit_reader_t *bs, bs_t *bd)
|
|
{
|
|
unsigned cb, bits;
|
|
int bit_count = remaining_bits(bs);
|
|
while (bit_count > 7)
|
|
{
|
|
cb = MINIMP4_MIN(bit_count - 7, 8);
|
|
bits = GetBits(cb);
|
|
h264e_bs_put_bits(bd, cb, bits);
|
|
bit_count -= cb;
|
|
}
|
|
|
|
// cut extra zeros after stop-bit
|
|
bits = GetBits(bit_count);
|
|
for (; bit_count && ~bits & 1; bit_count--)
|
|
{
|
|
bits >>= 1;
|
|
}
|
|
if (bit_count)
|
|
{
|
|
h264e_bs_put_bits(bd, bit_count, bits);
|
|
}
|
|
}
|
|
|
|
static int change_sps_id(bit_reader_t *bs, bs_t *bd, int new_id, int *old_id)
|
|
{
|
|
unsigned bits, sps_id, i, bytes;
|
|
for (i = 0; i < 3; i++)
|
|
{
|
|
bits = GetBits(8);
|
|
h264e_bs_put_bits(bd, 8, bits);
|
|
}
|
|
sps_id = ue_bits(bs); // max = 31
|
|
|
|
*old_id = sps_id;
|
|
sps_id = new_id;
|
|
assert(sps_id <= 31);
|
|
|
|
h264e_bs_put_golomb(bd, sps_id);
|
|
copy_bits(bs, bd);
|
|
|
|
bytes = h264e_bs_byte_align(bd) / 8;
|
|
h264e_bs_flush(bd);
|
|
return bytes;
|
|
}
|
|
|
|
static int patch_pps(h264_sps_id_patcher_t *h, bit_reader_t *bs, bs_t *bd, int new_pps_id, int *old_id)
|
|
{
|
|
int bytes;
|
|
unsigned pps_id = ue_bits(bs); // max = 255
|
|
unsigned sps_id = ue_bits(bs); // max = 31
|
|
|
|
*old_id = pps_id;
|
|
sps_id = h->map_sps[sps_id];
|
|
pps_id = new_pps_id;
|
|
|
|
assert(sps_id <= 31);
|
|
assert(pps_id <= 255);
|
|
|
|
h264e_bs_put_golomb(bd, pps_id);
|
|
h264e_bs_put_golomb(bd, sps_id);
|
|
copy_bits(bs, bd);
|
|
|
|
bytes = h264e_bs_byte_align(bd) / 8;
|
|
h264e_bs_flush(bd);
|
|
return bytes;
|
|
}
|
|
|
|
static void patch_slice_header(h264_sps_id_patcher_t *h, bit_reader_t *bs, bs_t *bd)
|
|
{
|
|
unsigned first_mb_in_slice = ue_bits(bs);
|
|
unsigned slice_type = ue_bits(bs);
|
|
unsigned pps_id = ue_bits(bs);
|
|
|
|
pps_id = h->map_pps[pps_id];
|
|
|
|
assert(pps_id <= 255);
|
|
|
|
h264e_bs_put_golomb(bd, first_mb_in_slice);
|
|
h264e_bs_put_golomb(bd, slice_type);
|
|
h264e_bs_put_golomb(bd, pps_id);
|
|
copy_bits(bs, bd);
|
|
}
|
|
|
|
static int transcode_nalu(h264_sps_id_patcher_t *h, const unsigned char *src, int nalu_bytes, unsigned char *dst)
|
|
{
|
|
int old_id;
|
|
|
|
bit_reader_t bst[1];
|
|
bs_t bdt[1];
|
|
|
|
bit_reader_t bs[1];
|
|
bs_t bd[1];
|
|
int payload_type = src[0] & 31;
|
|
|
|
*dst = *src;
|
|
h264e_bs_init_bits(bd, dst + 1);
|
|
init_bits(bs, src + 1, nalu_bytes - 1);
|
|
h264e_bs_init_bits(bdt, dst + 1);
|
|
init_bits(bst, src + 1, nalu_bytes - 1);
|
|
|
|
switch(payload_type)
|
|
{
|
|
case 7:
|
|
{
|
|
int cb = change_sps_id(bst, bdt, 0, &old_id);
|
|
int id = find_mem_cache(h->sps_cache, h->sps_bytes, MINIMP4_MAX_SPS, dst + 1, cb);
|
|
if (id == -1)
|
|
return 0;
|
|
h->map_sps[old_id] = id;
|
|
change_sps_id(bs, bd, id, &old_id);
|
|
}
|
|
break;
|
|
case 8:
|
|
{
|
|
int cb = patch_pps(h, bst, bdt, 0, &old_id);
|
|
int id = find_mem_cache(h->pps_cache, h->pps_bytes, MINIMP4_MAX_PPS, dst + 1, cb);
|
|
if (id == -1)
|
|
return 0;
|
|
h->map_pps[old_id] = id;
|
|
patch_pps(h, bs, bd, id, &old_id);
|
|
}
|
|
break;
|
|
case 1:
|
|
case 2:
|
|
case 5:
|
|
patch_slice_header(h, bs, bd);
|
|
break;
|
|
default:
|
|
memcpy(dst, src, nalu_bytes);
|
|
return nalu_bytes;
|
|
}
|
|
|
|
nalu_bytes = 1 + h264e_bs_byte_align(bd) / 8;
|
|
h264e_bs_flush(bd);
|
|
|
|
return nalu_bytes;
|
|
}
|
|
|
|
#endif
|
|
|
|
/**
|
|
* Set pointer just after start code (00 .. 00 01), or to EOF if not found:
|
|
*
|
|
* NZ NZ ... NZ 00 00 00 00 01 xx xx ... xx (EOF)
|
|
* ^ ^
|
|
* non-zero head.............. here ....... or here if no start code found
|
|
*
|
|
*/
|
|
static const uint8_t *find_start_code(const uint8_t *h264_data, int h264_data_bytes, int *zcount)
|
|
{
|
|
const uint8_t *eof = h264_data + h264_data_bytes;
|
|
const uint8_t *p = h264_data;
|
|
do
|
|
{
|
|
int zero_cnt = 1;
|
|
const uint8_t* found = (uint8_t*)memchr(p, 0, eof - p);
|
|
p = found ? found : eof;
|
|
while (p + zero_cnt < eof && !p[zero_cnt]) zero_cnt++;
|
|
if (zero_cnt >= 2 && p[zero_cnt] == 1)
|
|
{
|
|
*zcount = zero_cnt + 1;
|
|
return p + zero_cnt + 1;
|
|
}
|
|
p += zero_cnt;
|
|
} while (p < eof);
|
|
*zcount = 0;
|
|
return eof;
|
|
}
|
|
|
|
/**
|
|
* Locate NAL unit in given buffer, and calculate it's length
|
|
*/
|
|
static const uint8_t *find_nal_unit(const uint8_t *h264_data, int h264_data_bytes, int *pnal_unit_bytes)
|
|
{
|
|
const uint8_t *eof = h264_data + h264_data_bytes;
|
|
int zcount;
|
|
const uint8_t *start = find_start_code(h264_data, h264_data_bytes, &zcount);
|
|
const uint8_t *stop = start;
|
|
if (start)
|
|
{
|
|
stop = find_start_code(start, (int)(eof - start), &zcount);
|
|
while (stop > start && !stop[-1])
|
|
{
|
|
stop--;
|
|
}
|
|
}
|
|
|
|
*pnal_unit_bytes = (int)(stop - start - zcount);
|
|
return start;
|
|
}
|
|
|
|
int mp4_h26x_write_init(mp4_h26x_writer_t *h, MP4E_mux_t *mux, int width, int height, int is_hevc)
|
|
{
|
|
MP4E_track_t tr;
|
|
tr.track_media_kind = e_video;
|
|
tr.language[0] = 'u';
|
|
tr.language[1] = 'n';
|
|
tr.language[2] = 'd';
|
|
tr.language[3] = 0;
|
|
tr.object_type_indication = is_hevc ? MP4_OBJECT_TYPE_HEVC : MP4_OBJECT_TYPE_AVC;
|
|
tr.time_scale = 90000;
|
|
tr.default_duration = 0;
|
|
tr.u.v.width = width;
|
|
tr.u.v.height = height;
|
|
h->mux_track_id = MP4E_add_track(mux, &tr);
|
|
h->mux = mux;
|
|
|
|
h->is_hevc = is_hevc;
|
|
h->need_vps = is_hevc;
|
|
h->need_sps = 1;
|
|
h->need_pps = 1;
|
|
h->need_idr = 1;
|
|
#if MINIMP4_TRANSCODE_SPS_ID
|
|
memset(&h->sps_patcher, 0, sizeof(h264_sps_id_patcher_t));
|
|
#endif
|
|
return MP4E_STATUS_OK;
|
|
}
|
|
|
|
void mp4_h26x_write_close(mp4_h26x_writer_t *h)
|
|
{
|
|
#if MINIMP4_TRANSCODE_SPS_ID
|
|
h264_sps_id_patcher_t *p = &h->sps_patcher;
|
|
int i;
|
|
for (i = 0; i < MINIMP4_MAX_SPS; i++)
|
|
{
|
|
if (p->sps_cache[i])
|
|
free(p->sps_cache[i]);
|
|
}
|
|
for (i = 0; i < MINIMP4_MAX_PPS; i++)
|
|
{
|
|
if (p->pps_cache[i])
|
|
free(p->pps_cache[i]);
|
|
}
|
|
#endif
|
|
memset(h, 0, sizeof(*h));
|
|
}
|
|
|
|
static int mp4_h265_write_nal(mp4_h26x_writer_t *h, const unsigned char *nal, int sizeof_nal, unsigned timeStamp90kHz_next)
|
|
{
|
|
int payload_type = (nal[0] >> 1) & 0x3f;
|
|
int is_intra = payload_type >= HEVC_NAL_BLA_W_LP && payload_type <= HEVC_NAL_CRA_NUT;
|
|
int err = MP4E_STATUS_OK;
|
|
//printf("payload_type=%d, intra=%d\n", payload_type, is_intra);
|
|
|
|
if (is_intra && !h->need_sps && !h->need_pps && !h->need_vps)
|
|
h->need_idr = 0;
|
|
switch (payload_type)
|
|
{
|
|
case HEVC_NAL_VPS:
|
|
MP4E_set_vps(h->mux, h->mux_track_id, nal, sizeof_nal);
|
|
h->need_vps = 0;
|
|
break;
|
|
case HEVC_NAL_SPS:
|
|
MP4E_set_sps(h->mux, h->mux_track_id, nal, sizeof_nal);
|
|
h->need_sps = 0;
|
|
break;
|
|
case HEVC_NAL_PPS:
|
|
MP4E_set_pps(h->mux, h->mux_track_id, nal, sizeof_nal);
|
|
h->need_pps = 0;
|
|
break;
|
|
default:
|
|
if (h->need_vps || h->need_sps || h->need_pps || h->need_idr)
|
|
return MP4E_STATUS_BAD_ARGUMENTS;
|
|
{
|
|
unsigned char *tmp = (unsigned char *)malloc(4 + sizeof_nal);
|
|
if (!tmp)
|
|
return MP4E_STATUS_NO_MEMORY;
|
|
int sample_kind = MP4E_SAMPLE_DEFAULT;
|
|
tmp[0] = (unsigned char)(sizeof_nal >> 24);
|
|
tmp[1] = (unsigned char)(sizeof_nal >> 16);
|
|
tmp[2] = (unsigned char)(sizeof_nal >> 8);
|
|
tmp[3] = (unsigned char)(sizeof_nal);
|
|
memcpy(tmp + 4, nal, sizeof_nal);
|
|
if (is_intra)
|
|
sample_kind = MP4E_SAMPLE_RANDOM_ACCESS;
|
|
err = MP4E_put_sample(h->mux, h->mux_track_id, tmp, 4 + sizeof_nal, timeStamp90kHz_next, sample_kind);
|
|
free(tmp);
|
|
}
|
|
break;
|
|
}
|
|
return err;
|
|
}
|
|
|
|
int mp4_h26x_write_nal(mp4_h26x_writer_t *h, const unsigned char *nal, int length, unsigned timeStamp90kHz_next)
|
|
{
|
|
const unsigned char *eof = nal + length;
|
|
int payload_type, sizeof_nal, err = MP4E_STATUS_OK;
|
|
for (;;nal++)
|
|
{
|
|
#if MINIMP4_TRANSCODE_SPS_ID
|
|
unsigned char *nal1, *nal2;
|
|
#endif
|
|
nal = find_nal_unit(nal, (int)(eof - nal), &sizeof_nal);
|
|
if (!sizeof_nal)
|
|
break;
|
|
if (h->is_hevc)
|
|
{
|
|
ERR(mp4_h265_write_nal(h, nal, sizeof_nal, timeStamp90kHz_next));
|
|
continue;
|
|
}
|
|
payload_type = nal[0] & 31;
|
|
if (9 == payload_type)
|
|
continue; // access unit delimiter, nothing to be done
|
|
#if MINIMP4_TRANSCODE_SPS_ID
|
|
// Transcode SPS, PPS and slice headers, reassigning ID's for SPS and PPS:
|
|
// - assign unique ID's to different SPS and PPS
|
|
// - assign same ID's to equal (except ID) SPS and PPS
|
|
// - save all different SPS and PPS
|
|
nal1 = (unsigned char *)malloc(sizeof_nal*17/16 + 32);
|
|
if (!nal1)
|
|
return MP4E_STATUS_NO_MEMORY;
|
|
nal2 = (unsigned char *)malloc(sizeof_nal*17/16 + 32);
|
|
if (!nal2)
|
|
{
|
|
free(nal1);
|
|
return MP4E_STATUS_NO_MEMORY;
|
|
}
|
|
sizeof_nal = remove_nal_escapes(nal2, nal, sizeof_nal);
|
|
if (!sizeof_nal)
|
|
{
|
|
exit_with_free:
|
|
free(nal1);
|
|
free(nal2);
|
|
return MP4E_STATUS_BAD_ARGUMENTS;
|
|
}
|
|
|
|
sizeof_nal = transcode_nalu(&h->sps_patcher, nal2, sizeof_nal, nal1);
|
|
sizeof_nal = nal_put_esc(nal2, nal1, sizeof_nal);
|
|
|
|
switch (payload_type) {
|
|
case 7:
|
|
MP4E_set_sps(h->mux, h->mux_track_id, nal2 + 4, sizeof_nal - 4);
|
|
h->need_sps = 0;
|
|
break;
|
|
case 8:
|
|
if (h->need_sps)
|
|
goto exit_with_free;
|
|
MP4E_set_pps(h->mux, h->mux_track_id, nal2 + 4, sizeof_nal - 4);
|
|
h->need_pps = 0;
|
|
break;
|
|
case 5:
|
|
if (h->need_sps)
|
|
goto exit_with_free;
|
|
h->need_idr = 0;
|
|
// flow through
|
|
default:
|
|
if (h->need_sps)
|
|
goto exit_with_free;
|
|
if (!h->need_pps && !h->need_idr)
|
|
{
|
|
bit_reader_t bs[1];
|
|
init_bits(bs, nal + 1, sizeof_nal - 4 - 1);
|
|
unsigned first_mb_in_slice = ue_bits(bs);
|
|
//unsigned slice_type = ue_bits(bs);
|
|
int sample_kind = MP4E_SAMPLE_DEFAULT;
|
|
nal2[0] = (unsigned char)((sizeof_nal - 4) >> 24);
|
|
nal2[1] = (unsigned char)((sizeof_nal - 4) >> 16);
|
|
nal2[2] = (unsigned char)((sizeof_nal - 4) >> 8);
|
|
nal2[3] = (unsigned char)((sizeof_nal - 4));
|
|
if (first_mb_in_slice)
|
|
sample_kind = MP4E_SAMPLE_CONTINUATION;
|
|
else if (payload_type == 5)
|
|
sample_kind = MP4E_SAMPLE_RANDOM_ACCESS;
|
|
err = MP4E_put_sample(h->mux, h->mux_track_id, nal2, sizeof_nal, timeStamp90kHz_next, sample_kind);
|
|
}
|
|
break;
|
|
}
|
|
free(nal1);
|
|
free(nal2);
|
|
#else
|
|
// No SPS/PPS transcoding
|
|
// This branch assumes that encoder use correct SPS/PPS ID's
|
|
switch (payload_type) {
|
|
case 7:
|
|
MP4E_set_sps(h->mux, h->mux_track_id, nal, sizeof_nal);
|
|
h->need_sps = 0;
|
|
break;
|
|
case 8:
|
|
MP4E_set_pps(h->mux, h->mux_track_id, nal, sizeof_nal);
|
|
h->need_pps = 0;
|
|
break;
|
|
case 5:
|
|
if (h->need_sps)
|
|
return MP4E_STATUS_BAD_ARGUMENTS;
|
|
h->need_idr = 0;
|
|
// flow through
|
|
default:
|
|
if (h->need_sps)
|
|
return MP4E_STATUS_BAD_ARGUMENTS;
|
|
if (!h->need_pps && !h->need_idr)
|
|
{
|
|
bit_reader_t bs[1];
|
|
unsigned char *tmp = (unsigned char *)malloc(4 + sizeof_nal);
|
|
if (!tmp)
|
|
return MP4E_STATUS_NO_MEMORY;
|
|
init_bits(bs, nal + 1, sizeof_nal - 1);
|
|
unsigned first_mb_in_slice = ue_bits(bs);
|
|
int sample_kind = MP4E_SAMPLE_DEFAULT;
|
|
tmp[0] = (unsigned char)(sizeof_nal >> 24);
|
|
tmp[1] = (unsigned char)(sizeof_nal >> 16);
|
|
tmp[2] = (unsigned char)(sizeof_nal >> 8);
|
|
tmp[3] = (unsigned char)(sizeof_nal);
|
|
memcpy(tmp + 4, nal, sizeof_nal);
|
|
if (first_mb_in_slice)
|
|
sample_kind = MP4E_SAMPLE_CONTINUATION;
|
|
else if (payload_type == 5)
|
|
sample_kind = MP4E_SAMPLE_RANDOM_ACCESS;
|
|
err = MP4E_put_sample(h->mux, h->mux_track_id, tmp, 4 + sizeof_nal, timeStamp90kHz_next, sample_kind);
|
|
free(tmp);
|
|
}
|
|
break;
|
|
}
|
|
#endif
|
|
if (err)
|
|
break;
|
|
}
|
|
return err;
|
|
}
|
|
|
|
#if MP4D_TRACE_SUPPORTED
|
|
# define TRACE(x) printf x
|
|
#else
|
|
# define TRACE(x)
|
|
#endif
|
|
|
|
#define NELEM(x) (sizeof(x) / sizeof((x)[0]))
|
|
|
|
static int minimp4_fgets(MP4D_demux_t *mp4)
|
|
{
|
|
uint8_t c;
|
|
if (mp4->read_callback(mp4->read_pos, &c, 1, mp4->token))
|
|
return -1;
|
|
mp4->read_pos++;
|
|
return c;
|
|
}
|
|
|
|
/**
|
|
* Read given number of bytes from input stream
|
|
* Used to read box headers
|
|
*/
|
|
static unsigned minimp4_read(MP4D_demux_t *mp4, int nb, int *eof_flag)
|
|
{
|
|
uint32_t v = 0; int last_byte;
|
|
switch (nb)
|
|
{
|
|
case 4: v = (v << 8) | minimp4_fgets(mp4);
|
|
case 3: v = (v << 8) | minimp4_fgets(mp4);
|
|
case 2: v = (v << 8) | minimp4_fgets(mp4);
|
|
default:
|
|
case 1: v = (v << 8) | (last_byte = minimp4_fgets(mp4));
|
|
}
|
|
if (last_byte < 0)
|
|
{
|
|
*eof_flag = 1;
|
|
}
|
|
return v;
|
|
}
|
|
|
|
/**
|
|
* Read given number of bytes, but no more than *payload_bytes specifies...
|
|
* Used to read box payload
|
|
*/
|
|
static uint32_t read_payload(MP4D_demux_t *mp4, unsigned nb, boxsize_t *payload_bytes, int *eof_flag)
|
|
{
|
|
if (*payload_bytes < nb)
|
|
{
|
|
*eof_flag = 1;
|
|
nb = (int)*payload_bytes;
|
|
}
|
|
*payload_bytes -= nb;
|
|
|
|
return minimp4_read(mp4, nb, eof_flag);
|
|
}
|
|
|
|
/**
|
|
* Skips given number of bytes.
|
|
* Avoid math operations with fpos_t
|
|
*/
|
|
static void my_fseek(MP4D_demux_t *mp4, boxsize_t pos, int *eof_flag)
|
|
{
|
|
mp4->read_pos += pos;
|
|
if (mp4->read_pos >= mp4->read_size)
|
|
*eof_flag = 1;
|
|
}
|
|
|
|
#define READ(n) read_payload(mp4, n, &payload_bytes, &eof_flag)
|
|
#define SKIP(n) { boxsize_t t = MINIMP4_MIN(payload_bytes, n); my_fseek(mp4, t, &eof_flag); payload_bytes -= t; }
|
|
#define MALLOC(t, p, size) p = (t)malloc(size); if (!(p)) { ERROR("out of memory"); }
|
|
|
|
/*
|
|
* On error: release resources.
|
|
*/
|
|
#define RETURN_ERROR(mess) { \
|
|
TRACE(("\nMP4 ERROR: " mess)); \
|
|
MP4D_close(mp4); \
|
|
return 0; \
|
|
}
|
|
|
|
/*
|
|
* Any errors, occurred on top-level hierarchy is passed to exit check: 'if (!mp4->track_count) ... '
|
|
*/
|
|
#define ERROR(mess) \
|
|
if (!depth) \
|
|
break; \
|
|
else \
|
|
RETURN_ERROR(mess);
|
|
|
|
typedef enum { BOX_ATOM, BOX_OD } boxtype_t;
|
|
|
|
int MP4D_open(MP4D_demux_t *mp4, int (*read_callback)(int64_t offset, void *buffer, size_t size, void *token), void *token, int64_t file_size)
|
|
{
|
|
// box stack size
|
|
int depth = 0;
|
|
|
|
struct
|
|
{
|
|
// remaining bytes for box in the stack
|
|
boxsize_t bytes;
|
|
|
|
// kind of box children's: OD chunks handled in the same manner as name chunks
|
|
boxtype_t format;
|
|
|
|
} stack[MAX_CHUNKS_DEPTH];
|
|
|
|
#if MP4D_TRACE_SUPPORTED
|
|
// path of current element: List0/List1/... etc
|
|
uint32_t box_path[MAX_CHUNKS_DEPTH];
|
|
#endif
|
|
|
|
int eof_flag = 0;
|
|
unsigned i;
|
|
MP4D_track_t *tr = NULL;
|
|
|
|
if (!mp4 || !read_callback)
|
|
{
|
|
TRACE(("\nERROR: invlaid arguments!"));
|
|
return 0;
|
|
}
|
|
|
|
memset(mp4, 0, sizeof(MP4D_demux_t));
|
|
mp4->read_callback = read_callback;
|
|
mp4->token = token;
|
|
mp4->read_size = file_size;
|
|
|
|
stack[0].format = BOX_ATOM; // start with atom box
|
|
stack[0].bytes = 0; // never accessed
|
|
|
|
do
|
|
{
|
|
// List of boxes, derived from 'FullBox'
|
|
// ~~~~~~~~~~~~~~~~~~~~~
|
|
// need read version field and check version for these boxes
|
|
static const struct
|
|
{
|
|
uint32_t name;
|
|
unsigned max_version;
|
|
unsigned use_track_flag;
|
|
} g_fullbox[] =
|
|
{
|
|
#if MP4D_INFO_SUPPORTED
|
|
{BOX_mdhd, 1, 1},
|
|
{BOX_mvhd, 1, 0},
|
|
{BOX_hdlr, 0, 0},
|
|
{BOX_meta, 0, 0}, // Android can produce meta box without 'FullBox' field, comment this line to simulate the bug
|
|
#endif
|
|
#if MP4D_TRACE_TIMESTAMPS
|
|
{BOX_stts, 0, 0},
|
|
{BOX_ctts, 0, 0},
|
|
#endif
|
|
{BOX_stz2, 0, 1},
|
|
{BOX_stsz, 0, 1},
|
|
{BOX_stsc, 0, 1},
|
|
{BOX_stco, 0, 1},
|
|
{BOX_co64, 0, 1},
|
|
{BOX_stsd, 0, 0},
|
|
{BOX_esds, 0, 1} // esds does not use track, but switches to OD mode. Check here, to avoid OD check
|
|
};
|
|
|
|
// List of boxes, which contains other boxes ('envelopes')
|
|
// Parser will descend down for boxes in this list, otherwise parsing will proceed to
|
|
// the next sibling box
|
|
// OD boxes handled in the same way as atom boxes...
|
|
static const struct
|
|
{
|
|
uint32_t name;
|
|
boxtype_t type;
|
|
} g_envelope_box[] =
|
|
{
|
|
{BOX_esds, BOX_OD}, // TODO: BOX_esds can be used for both audio and video, but this code supports audio only!
|
|
{OD_ESD, BOX_OD},
|
|
{OD_DCD, BOX_OD},
|
|
{OD_DSI, BOX_OD},
|
|
{BOX_trak, BOX_ATOM},
|
|
{BOX_moov, BOX_ATOM},
|
|
//{BOX_moof, BOX_ATOM},
|
|
{BOX_mdia, BOX_ATOM},
|
|
{BOX_tref, BOX_ATOM},
|
|
{BOX_minf, BOX_ATOM},
|
|
{BOX_dinf, BOX_ATOM},
|
|
{BOX_stbl, BOX_ATOM},
|
|
{BOX_stsd, BOX_ATOM},
|
|
{BOX_mp4a, BOX_ATOM},
|
|
{BOX_mp4s, BOX_ATOM},
|
|
#if MP4D_AVC_SUPPORTED
|
|
{BOX_mp4v, BOX_ATOM},
|
|
{BOX_avc1, BOX_ATOM},
|
|
//{BOX_avc2, BOX_ATOM},
|
|
//{BOX_svc1, BOX_ATOM},
|
|
#endif
|
|
#if MP4D_HEVC_SUPPORTED
|
|
{BOX_hvc1, BOX_ATOM},
|
|
#endif
|
|
{BOX_udta, BOX_ATOM},
|
|
{BOX_meta, BOX_ATOM},
|
|
{BOX_ilst, BOX_ATOM}
|
|
};
|
|
|
|
uint32_t FullAtomVersionAndFlags = 0;
|
|
boxsize_t payload_bytes;
|
|
boxsize_t box_bytes;
|
|
uint32_t box_name;
|
|
#if MP4D_INFO_SUPPORTED
|
|
unsigned char **ptag = NULL;
|
|
#endif
|
|
int read_bytes = 0;
|
|
|
|
// Read header box type and it's length
|
|
if (stack[depth].format == BOX_ATOM)
|
|
{
|
|
box_bytes = minimp4_read(mp4, 4, &eof_flag);
|
|
#if FIX_BAD_ANDROID_META_BOX
|
|
broken_android_meta_hack:
|
|
#endif
|
|
if (eof_flag)
|
|
break; // normal exit
|
|
|
|
if (box_bytes >= 2 && box_bytes < 8)
|
|
{
|
|
ERROR("invalid box size (broken file?)");
|
|
}
|
|
|
|
box_name = minimp4_read(mp4, 4, &eof_flag);
|
|
read_bytes = 8;
|
|
|
|
// Decode box size
|
|
if (box_bytes == 0 || // standard indication of 'till eof' size
|
|
box_bytes == (boxsize_t)0xFFFFFFFFU // some files uses non-standard 'till eof' signaling
|
|
)
|
|
{
|
|
box_bytes = ~(boxsize_t)0;
|
|
}
|
|
|
|
payload_bytes = box_bytes - 8;
|
|
|
|
if (box_bytes == 1) // 64-bit sizes
|
|
{
|
|
TRACE(("\n64-bit chunk encountered"));
|
|
|
|
box_bytes = minimp4_read(mp4, 4, &eof_flag);
|
|
#if MP4D_64BIT_SUPPORTED
|
|
box_bytes <<= 32;
|
|
box_bytes |= minimp4_read(mp4, 4, &eof_flag);
|
|
#else
|
|
if (box_bytes)
|
|
{
|
|
ERROR("UNSUPPORTED FEATURE: MP4BoxHeader(): 64-bit boxes not supported!");
|
|
}
|
|
box_bytes = minimp4_read(mp4, 4, &eof_flag);
|
|
#endif
|
|
if (box_bytes < 16)
|
|
{
|
|
ERROR("invalid box size (broken file?)");
|
|
}
|
|
payload_bytes = box_bytes - 16;
|
|
}
|
|
|
|
// Read and check box version for some boxes
|
|
for (i = 0; i < NELEM(g_fullbox); i++)
|
|
{
|
|
if (box_name == g_fullbox[i].name)
|
|
{
|
|
FullAtomVersionAndFlags = READ(4);
|
|
read_bytes += 4;
|
|
|
|
#if FIX_BAD_ANDROID_META_BOX
|
|
// Fix invalid BOX_meta, found in some Android-produced MP4
|
|
// This branch is optional: bad box would be skipped
|
|
if (box_name == BOX_meta)
|
|
{
|
|
if (FullAtomVersionAndFlags >= 8 && FullAtomVersionAndFlags < payload_bytes)
|
|
{
|
|
if (box_bytes > stack[depth].bytes)
|
|
{
|
|
ERROR("broken file structure!");
|
|
}
|
|
stack[depth].bytes -= box_bytes;;
|
|
depth++;
|
|
stack[depth].bytes = payload_bytes + 4; // +4 need for missing header
|
|
stack[depth].format = BOX_ATOM;
|
|
box_bytes = FullAtomVersionAndFlags;
|
|
TRACE(("Bad metadata box detected (Android bug?)!\n"));
|
|
goto broken_android_meta_hack;
|
|
}
|
|
}
|
|
#endif // FIX_BAD_ANDROID_META_BOX
|
|
|
|
if ((FullAtomVersionAndFlags >> 24) > g_fullbox[i].max_version)
|
|
{
|
|
ERROR("unsupported box version!");
|
|
}
|
|
if (g_fullbox[i].use_track_flag && !tr)
|
|
{
|
|
ERROR("broken file structure!");
|
|
}
|
|
}
|
|
}
|
|
} else // stack[depth].format == BOX_OD
|
|
{
|
|
int val;
|
|
box_name = OD_BASE + minimp4_read(mp4, 1, &eof_flag); // 1-byte box type
|
|
read_bytes += 1;
|
|
if (eof_flag)
|
|
break;
|
|
|
|
payload_bytes = 0;
|
|
box_bytes = 1;
|
|
do
|
|
{
|
|
val = minimp4_read(mp4, 1, &eof_flag);
|
|
read_bytes += 1;
|
|
if (eof_flag)
|
|
{
|
|
ERROR("premature EOF!");
|
|
}
|
|
payload_bytes = (payload_bytes << 7) | (val & 0x7F);
|
|
box_bytes++;
|
|
} while (val & 0x80);
|
|
box_bytes += payload_bytes;
|
|
}
|
|
|
|
#if MP4D_TRACE_SUPPORTED
|
|
box_path[depth] = (box_name >> 24) | (box_name << 24) | ((box_name >> 8) & 0x0000FF00) | ((box_name << 8) & 0x00FF0000);
|
|
TRACE(("%2d %8d %.*s (%d bytes remains for sibilings) \n", depth, (int)box_bytes, depth*4, (char*)box_path, (int)stack[depth].bytes));
|
|
#endif
|
|
|
|
// Check that box size <= parent size
|
|
if (depth)
|
|
{
|
|
// Skip box with bad size
|
|
assert(box_bytes > 0);
|
|
if (box_bytes > stack[depth].bytes)
|
|
{
|
|
TRACE(("Wrong %c%c%c%c box size: broken file?\n", (box_name >> 24)&255, (box_name >> 16)&255, (box_name >> 8)&255, box_name&255));
|
|
box_bytes = stack[depth].bytes;
|
|
box_name = 0;
|
|
payload_bytes = box_bytes - read_bytes;
|
|
}
|
|
stack[depth].bytes -= box_bytes;
|
|
}
|
|
|
|
// Read box header
|
|
switch(box_name)
|
|
{
|
|
case BOX_stz2: //ISO/IEC 14496-1 Page 38. Section 8.17.2 - Sample Size Box.
|
|
case BOX_stsz:
|
|
{
|
|
int size = 0;
|
|
uint32_t sample_size = READ(4);
|
|
tr->sample_count = READ(4);
|
|
MALLOC(unsigned int*, tr->entry_size, tr->sample_count*4);
|
|
for (i = 0; i < tr->sample_count; i++)
|
|
{
|
|
if (box_name == BOX_stsz)
|
|
{
|
|
tr->entry_size[i] = (sample_size?sample_size:READ(4));
|
|
} else
|
|
{
|
|
switch (sample_size & 0xFF)
|
|
{
|
|
case 16:
|
|
tr->entry_size[i] = READ(2);
|
|
break;
|
|
case 8:
|
|
tr->entry_size[i] = READ(1);
|
|
break;
|
|
case 4:
|
|
if (i & 1)
|
|
{
|
|
tr->entry_size[i] = size & 15;
|
|
} else
|
|
{
|
|
size = READ(1);
|
|
tr->entry_size[i] = (size >> 4);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case BOX_stsc: //ISO/IEC 14496-12 Page 38. Section 8.18 - Sample To Chunk Box.
|
|
tr->sample_to_chunk_count = READ(4);
|
|
MALLOC(MP4D_sample_to_chunk_t*, tr->sample_to_chunk, tr->sample_to_chunk_count*sizeof(tr->sample_to_chunk[0]));
|
|
for (i = 0; i < tr->sample_to_chunk_count; i++)
|
|
{
|
|
tr->sample_to_chunk[i].first_chunk = READ(4);
|
|
tr->sample_to_chunk[i].samples_per_chunk = READ(4);
|
|
SKIP(4); // sample_description_index
|
|
}
|
|
break;
|
|
#if MP4D_TRACE_TIMESTAMPS || MP4D_TIMESTAMPS_SUPPORTED
|
|
case BOX_stts:
|
|
{
|
|
unsigned count = READ(4);
|
|
unsigned j, k = 0, ts = 0, ts_count = count;
|
|
#if MP4D_TIMESTAMPS_SUPPORTED
|
|
MALLOC(unsigned int*, tr->timestamp, ts_count*4);
|
|
MALLOC(unsigned int*, tr->duration, ts_count*4);
|
|
#endif
|
|
|
|
for (i = 0; i < count; i++)
|
|
{
|
|
unsigned sc = READ(4);
|
|
int d = READ(4);
|
|
TRACE(("sample %8d count %8d duration %8d\n", i, sc, d));
|
|
#if MP4D_TIMESTAMPS_SUPPORTED
|
|
if (k + sc > ts_count)
|
|
{
|
|
ts_count = k + sc;
|
|
tr->timestamp = (unsigned int*)realloc(tr->timestamp, ts_count * sizeof(unsigned));
|
|
tr->duration = (unsigned int*)realloc(tr->duration, ts_count * sizeof(unsigned));
|
|
}
|
|
for (j = 0; j < sc; j++)
|
|
{
|
|
tr->duration[k] = d;
|
|
tr->timestamp[k++] = ts;
|
|
ts += d;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
break;
|
|
case BOX_ctts:
|
|
{
|
|
unsigned count = READ(4);
|
|
for (i = 0; i < count; i++)
|
|
{
|
|
int sc = READ(4);
|
|
int d = READ(4);
|
|
(void)sc;
|
|
(void)d;
|
|
TRACE(("sample %8d count %8d decoding to composition offset %8d\n", i, sc, d));
|
|
}
|
|
}
|
|
break;
|
|
#endif
|
|
case BOX_stco: //ISO/IEC 14496-12 Page 39. Section 8.19 - Chunk Offset Box.
|
|
case BOX_co64:
|
|
tr->chunk_count = READ(4);
|
|
MALLOC(MP4D_file_offset_t*, tr->chunk_offset, tr->chunk_count*sizeof(MP4D_file_offset_t));
|
|
for (i = 0; i < tr->chunk_count; i++)
|
|
{
|
|
tr->chunk_offset[i] = READ(4);
|
|
if (box_name == BOX_co64)
|
|
{
|
|
#if !MP4D_64BIT_SUPPORTED
|
|
if (tr->chunk_offset[i])
|
|
{
|
|
ERROR("UNSUPPORTED FEATURE: 64-bit chunk_offset not supported!");
|
|
}
|
|
#endif
|
|
tr->chunk_offset[i] <<= 32;
|
|
tr->chunk_offset[i] |= READ(4);
|
|
}
|
|
}
|
|
break;
|
|
|
|
#if MP4D_INFO_SUPPORTED
|
|
case BOX_mvhd:
|
|
SKIP(((FullAtomVersionAndFlags >> 24) == 1) ? 8 + 8 : 4 + 4);
|
|
mp4->timescale = READ(4);
|
|
mp4->duration_hi = ((FullAtomVersionAndFlags >> 24) == 1) ? READ(4) : 0;
|
|
mp4->duration_lo = READ(4);
|
|
SKIP(4 + 2 + 2 + 4*2 + 4*9 + 4*6 + 4);
|
|
break;
|
|
|
|
case BOX_mdhd:
|
|
SKIP(((FullAtomVersionAndFlags >> 24) == 1) ? 8 + 8 : 4 + 4);
|
|
tr->timescale = READ(4);
|
|
tr->duration_hi = ((FullAtomVersionAndFlags >> 24) == 1) ? READ(4) : 0;
|
|
tr->duration_lo = READ(4);
|
|
|
|
{
|
|
int ISO_639_2_T = READ(2);
|
|
tr->language[2] = (ISO_639_2_T & 31) + 0x60; ISO_639_2_T >>= 5;
|
|
tr->language[1] = (ISO_639_2_T & 31) + 0x60; ISO_639_2_T >>= 5;
|
|
tr->language[0] = (ISO_639_2_T & 31) + 0x60;
|
|
}
|
|
// the rest of this box is skipped by default ...
|
|
break;
|
|
|
|
case BOX_hdlr:
|
|
if (tr) // When this box is within 'meta' box, the track may not be avaialable
|
|
{
|
|
SKIP(4); // pre_defined
|
|
tr->handler_type = READ(4);
|
|
}
|
|
// typically hdlr box does not contain any useful info.
|
|
// the rest of this box is skipped by default ...
|
|
break;
|
|
|
|
case BOX_btrt:
|
|
if (!tr)
|
|
{
|
|
ERROR("broken file structure!");
|
|
}
|
|
|
|
SKIP(4 + 4);
|
|
tr->avg_bitrate_bps = READ(4);
|
|
break;
|
|
|
|
// Set pointer to tag to be read...
|
|
case BOX_calb: ptag = &mp4->tag.album; break;
|
|
case BOX_cART: ptag = &mp4->tag.artist; break;
|
|
case BOX_cnam: ptag = &mp4->tag.title; break;
|
|
case BOX_cday: ptag = &mp4->tag.year; break;
|
|
case BOX_ccmt: ptag = &mp4->tag.comment; break;
|
|
case BOX_cgen: ptag = &mp4->tag.genre; break;
|
|
|
|
#endif
|
|
|
|
case BOX_stsd:
|
|
SKIP(4); // entry_count, BOX_mp4a & BOX_mp4v boxes follows immediately
|
|
break;
|
|
|
|
case BOX_mp4s: // private stream
|
|
if (!tr)
|
|
{
|
|
ERROR("broken file structure!");
|
|
}
|
|
SKIP(6*1 + 2/*Base SampleEntry*/);
|
|
break;
|
|
|
|
case BOX_mp4a:
|
|
if (!tr)
|
|
{
|
|
ERROR("broken file structure!");
|
|
}
|
|
#if MP4D_INFO_SUPPORTED
|
|
SKIP(6*1+2/*Base SampleEntry*/ + 4*2);
|
|
tr->SampleDescription.audio.channelcount = READ(2);
|
|
SKIP(2/*samplesize*/ + 2 + 2);
|
|
tr->SampleDescription.audio.samplerate_hz = READ(4) >> 16;
|
|
#else
|
|
SKIP(28);
|
|
#endif
|
|
break;
|
|
|
|
#if MP4D_AVC_SUPPORTED
|
|
case BOX_avc1: // AVCSampleEntry extends VisualSampleEntry
|
|
// case BOX_avc2: - no test
|
|
// case BOX_svc1: - no test
|
|
case BOX_mp4v:
|
|
if (!tr)
|
|
{
|
|
ERROR("broken file structure!");
|
|
}
|
|
#if MP4D_INFO_SUPPORTED
|
|
SKIP(6*1 + 2/*Base SampleEntry*/ + 2 + 2 + 4*3);
|
|
tr->SampleDescription.video.width = READ(2);
|
|
tr->SampleDescription.video.height = READ(2);
|
|
// frame_count is always 1
|
|
// compressorname is rarely set..
|
|
SKIP(4 + 4 + 4 + 2/*frame_count*/ + 32/*compressorname*/ + 2 + 2);
|
|
#else
|
|
SKIP(78);
|
|
#endif
|
|
// ^^^ end of VisualSampleEntry
|
|
// now follows for BOX_avc1:
|
|
// BOX_avcC
|
|
// BOX_btrt (optional)
|
|
// BOX_m4ds (optional)
|
|
// for BOX_mp4v:
|
|
// BOX_esds
|
|
break;
|
|
|
|
case BOX_avcC: // AVCDecoderConfigurationRecord()
|
|
// hack: AAC-specific DSI field reused (for it have same purpoose as sps/pps)
|
|
// TODO: check this hack if BOX_esds co-exist with BOX_avcC
|
|
tr->object_type_indication = MP4_OBJECT_TYPE_AVC;
|
|
tr->dsi = (unsigned char*)malloc((size_t)box_bytes);
|
|
tr->dsi_bytes = (unsigned)box_bytes;
|
|
{
|
|
int spspps;
|
|
unsigned char *p = tr->dsi;
|
|
unsigned int configurationVersion = READ(1);
|
|
unsigned int AVCProfileIndication = READ(1);
|
|
unsigned int profile_compatibility = READ(1);
|
|
unsigned int AVCLevelIndication = READ(1);
|
|
//bit(6) reserved =
|
|
unsigned int lengthSizeMinusOne = READ(1) & 3;
|
|
|
|
(void)configurationVersion;
|
|
(void)AVCProfileIndication;
|
|
(void)profile_compatibility;
|
|
(void)AVCLevelIndication;
|
|
(void)lengthSizeMinusOne;
|
|
|
|
for (spspps = 0; spspps < 2; spspps++)
|
|
{
|
|
unsigned int numOfSequenceParameterSets= READ(1);
|
|
if (!spspps)
|
|
{
|
|
numOfSequenceParameterSets &= 31; // clears 3 msb for SPS
|
|
}
|
|
*p++ = numOfSequenceParameterSets;
|
|
for (i = 0; i < numOfSequenceParameterSets; i++)
|
|
{
|
|
unsigned k, sequenceParameterSetLength = READ(2);
|
|
*p++ = sequenceParameterSetLength >> 8;
|
|
*p++ = sequenceParameterSetLength ;
|
|
for (k = 0; k < sequenceParameterSetLength; k++)
|
|
{
|
|
*p++ = READ(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
#endif // MP4D_AVC_SUPPORTED
|
|
|
|
case OD_ESD:
|
|
{
|
|
unsigned flags = READ(3); // ES_ID(2) + flags(1)
|
|
|
|
if (flags & 0x80) // steamdependflag
|
|
{
|
|
SKIP(2); // dependsOnESID
|
|
}
|
|
if (flags & 0x40) // urlflag
|
|
{
|
|
unsigned bytecount = READ(1);
|
|
SKIP(bytecount); // skip URL
|
|
}
|
|
if (flags & 0x20) // ocrflag (was reserved in MPEG-4 v.1)
|
|
{
|
|
SKIP(2); // OCRESID
|
|
}
|
|
break;
|
|
}
|
|
|
|
case OD_DCD: //ISO/IEC 14496-1 Page 28. Section 8.6.5 - DecoderConfigDescriptor.
|
|
assert(tr); // ensured by g_fullbox[] check
|
|
tr->object_type_indication = READ(1);
|
|
#if MP4D_INFO_SUPPORTED
|
|
tr->stream_type = READ(1) >> 2;
|
|
SKIP(3/*bufferSizeDB*/ + 4/*maxBitrate*/);
|
|
tr->avg_bitrate_bps = READ(4);
|
|
#else
|
|
SKIP(1+3+4+4);
|
|
#endif
|
|
break;
|
|
|
|
case OD_DSI: //ISO/IEC 14496-1 Page 28. Section 8.6.5 - DecoderConfigDescriptor.
|
|
assert(tr); // ensured by g_fullbox[] check
|
|
if (!tr->dsi && payload_bytes)
|
|
{
|
|
MALLOC(unsigned char*, tr->dsi, (int)payload_bytes);
|
|
for (i = 0; i < payload_bytes; i++)
|
|
{
|
|
tr->dsi[i] = minimp4_read(mp4, 1, &eof_flag); // These bytes available due to check above
|
|
}
|
|
tr->dsi_bytes = i;
|
|
payload_bytes -= i;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
TRACE(("[%c%c%c%c] %d\n", box_name >> 24, box_name >> 16, box_name >> 8, box_name, (int)payload_bytes));
|
|
}
|
|
|
|
#if MP4D_INFO_SUPPORTED
|
|
// Read tag is tag pointer is set
|
|
if (ptag && !*ptag && payload_bytes > 16)
|
|
{
|
|
#if 0
|
|
uint32_t size = READ(4);
|
|
uint32_t data = READ(4);
|
|
uint32_t class = READ(4);
|
|
uint32_t x1 = READ(4);
|
|
TRACE(("%2d %2d %2d ", size, class, x1));
|
|
#else
|
|
SKIP(4 + 4 + 4 + 4);
|
|
#endif
|
|
MALLOC(unsigned char*, *ptag, (unsigned)payload_bytes + 1);
|
|
for (i = 0; payload_bytes != 0; i++)
|
|
{
|
|
(*ptag)[i] = READ(1);
|
|
}
|
|
(*ptag)[i] = 0; // zero-terminated string
|
|
}
|
|
#endif
|
|
|
|
if (box_name == BOX_trak)
|
|
{
|
|
// New track found: allocate memory using realloc()
|
|
// Typically there are 1 audio track for AAC audio file,
|
|
// 4 tracks for movie file,
|
|
// 3-5 tracks for scalable audio (CELP+AAC)
|
|
// and up to 50 tracks for BSAC scalable audio
|
|
void *mem = realloc(mp4->track, (mp4->track_count + 1)*sizeof(MP4D_track_t));
|
|
if (!mem)
|
|
{
|
|
// if realloc fails, it does not deallocate old pointer!
|
|
ERROR("out of memory");
|
|
}
|
|
mp4->track = (MP4D_track_t*)mem;
|
|
tr = mp4->track + mp4->track_count++;
|
|
memset(tr, 0, sizeof(MP4D_track_t));
|
|
} else if (box_name == BOX_meta)
|
|
{
|
|
tr = NULL; // Avoid update of 'hdlr' box, which may contains in the 'meta' box
|
|
}
|
|
|
|
// If this box is envelope, save it's size in box stack
|
|
for (i = 0; i < NELEM(g_envelope_box); i++)
|
|
{
|
|
if (box_name == g_envelope_box[i].name)
|
|
{
|
|
if (++depth >= MAX_CHUNKS_DEPTH)
|
|
{
|
|
ERROR("too deep atoms nesting!");
|
|
}
|
|
stack[depth].bytes = payload_bytes;
|
|
stack[depth].format = g_envelope_box[i].type;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// if box is not envelope, just skip it
|
|
if (i == NELEM(g_envelope_box))
|
|
{
|
|
if (payload_bytes > file_size)
|
|
{
|
|
eof_flag = 1;
|
|
} else
|
|
{
|
|
SKIP(payload_bytes);
|
|
}
|
|
}
|
|
|
|
// remove empty boxes from stack
|
|
// don't touch box with index 0 (which indicates whole file)
|
|
while (depth > 0 && !stack[depth].bytes)
|
|
{
|
|
depth--;
|
|
}
|
|
|
|
} while(!eof_flag);
|
|
|
|
if (!mp4->track_count)
|
|
{
|
|
RETURN_ERROR("no tracks found");
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Find chunk, containing given sample.
|
|
* Returns chunk number, and first sample in this chunk.
|
|
*/
|
|
static int sample_to_chunk(MP4D_track_t *tr, unsigned nsample, unsigned *nfirst_sample_in_chunk)
|
|
{
|
|
unsigned chunk_group = 0, nc;
|
|
unsigned sum = 0;
|
|
*nfirst_sample_in_chunk = 0;
|
|
if (tr->chunk_count <= 1)
|
|
{
|
|
return 0;
|
|
}
|
|
for (nc = 0; nc < tr->chunk_count; nc++)
|
|
{
|
|
if (chunk_group + 1 < tr->sample_to_chunk_count // stuck at last entry till EOF
|
|
&& nc + 1 == // Chunks counted starting with '1'
|
|
tr->sample_to_chunk[chunk_group + 1].first_chunk) // next group?
|
|
{
|
|
chunk_group++;
|
|
}
|
|
|
|
sum += tr->sample_to_chunk[chunk_group].samples_per_chunk;
|
|
if (nsample < sum)
|
|
return nc;
|
|
|
|
// TODO: this can be calculated once per file
|
|
*nfirst_sample_in_chunk = sum;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Exported API function
|
|
MP4D_file_offset_t MP4D_frame_offset(const MP4D_demux_t *mp4, unsigned ntrack, unsigned nsample, unsigned *frame_bytes, unsigned *timestamp, unsigned *duration)
|
|
{
|
|
MP4D_track_t *tr = mp4->track + ntrack;
|
|
unsigned ns;
|
|
int nchunk = sample_to_chunk(tr, nsample, &ns);
|
|
MP4D_file_offset_t offset;
|
|
|
|
if (nchunk < 0)
|
|
{
|
|
*frame_bytes = 0;
|
|
return 0;
|
|
}
|
|
|
|
offset = tr->chunk_offset[nchunk];
|
|
for (; ns < nsample; ns++)
|
|
{
|
|
offset += tr->entry_size[ns];
|
|
}
|
|
|
|
*frame_bytes = tr->entry_size[ns];
|
|
|
|
if (timestamp)
|
|
{
|
|
#if MP4D_TIMESTAMPS_SUPPORTED
|
|
*timestamp = tr->timestamp[ns];
|
|
#else
|
|
*timestamp = 0;
|
|
#endif
|
|
}
|
|
if (duration)
|
|
{
|
|
#if MP4D_TIMESTAMPS_SUPPORTED
|
|
*duration = tr->duration[ns];
|
|
#else
|
|
*duration = 0;
|
|
#endif
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
#define FREE(x) if (x) {free(x); x = NULL;}
|
|
|
|
// Exported API function
|
|
void MP4D_close(MP4D_demux_t *mp4)
|
|
{
|
|
while (mp4->track_count)
|
|
{
|
|
MP4D_track_t *tr = mp4->track + --mp4->track_count;
|
|
FREE(tr->entry_size);
|
|
#if MP4D_TIMESTAMPS_SUPPORTED
|
|
FREE(tr->timestamp);
|
|
FREE(tr->duration);
|
|
#endif
|
|
FREE(tr->sample_to_chunk);
|
|
FREE(tr->chunk_offset);
|
|
FREE(tr->dsi);
|
|
}
|
|
FREE(mp4->track);
|
|
#if MP4D_INFO_SUPPORTED
|
|
FREE(mp4->tag.title);
|
|
FREE(mp4->tag.artist);
|
|
FREE(mp4->tag.album);
|
|
FREE(mp4->tag.year);
|
|
FREE(mp4->tag.comment);
|
|
FREE(mp4->tag.genre);
|
|
#endif
|
|
}
|
|
|
|
static int skip_spspps(const unsigned char *p, int nbytes, int nskip)
|
|
{
|
|
int i, k = 0;
|
|
for (i = 0; i < nskip; i++)
|
|
{
|
|
unsigned segmbytes;
|
|
if (k > nbytes - 2)
|
|
return -1;
|
|
segmbytes = p[k]*256 + p[k+1];
|
|
k += 2 + segmbytes;
|
|
}
|
|
return k;
|
|
}
|
|
|
|
static const void *MP4D_read_spspps(const MP4D_demux_t *mp4, unsigned int ntrack, int pps_flag, int nsps, int *sps_bytes)
|
|
{
|
|
int sps_count, skip_bytes;
|
|
int bytepos = 0;
|
|
unsigned char *p = mp4->track[ntrack].dsi;
|
|
if (ntrack >= mp4->track_count)
|
|
return NULL;
|
|
if (mp4->track[ntrack].object_type_indication != MP4_OBJECT_TYPE_AVC)
|
|
return NULL; // SPS/PPS are specific for AVC format only
|
|
|
|
if (pps_flag)
|
|
{
|
|
// Skip all SPS
|
|
sps_count = p[bytepos++];
|
|
skip_bytes = skip_spspps(p+bytepos, mp4->track[ntrack].dsi_bytes - bytepos, sps_count);
|
|
if (skip_bytes < 0)
|
|
return NULL;
|
|
bytepos += skip_bytes;
|
|
}
|
|
|
|
// Skip sps/pps before the given target
|
|
sps_count = p[bytepos++];
|
|
if (nsps >= sps_count)
|
|
return NULL;
|
|
skip_bytes = skip_spspps(p+bytepos, mp4->track[ntrack].dsi_bytes - bytepos, nsps);
|
|
if (skip_bytes < 0)
|
|
return NULL;
|
|
bytepos += skip_bytes;
|
|
*sps_bytes = p[bytepos]*256 + p[bytepos+1];
|
|
return p + bytepos + 2;
|
|
}
|
|
|
|
|
|
const void *MP4D_read_sps(const MP4D_demux_t *mp4, unsigned int ntrack, int nsps, int *sps_bytes)
|
|
{
|
|
return MP4D_read_spspps(mp4, ntrack, 0, nsps, sps_bytes);
|
|
}
|
|
|
|
const void *MP4D_read_pps(const MP4D_demux_t *mp4, unsigned int ntrack, int npps, int *pps_bytes)
|
|
{
|
|
return MP4D_read_spspps(mp4, ntrack, 1, npps, pps_bytes);
|
|
}
|
|
|
|
#if MP4D_PRINT_INFO_SUPPORTED
|
|
/************************************************************************/
|
|
/* Purely informational part, may be removed for embedded applications */
|
|
/************************************************************************/
|
|
|
|
//
|
|
// Decodes ISO/IEC 14496 MP4 stream type to ASCII string
|
|
//
|
|
static const char *GetMP4StreamTypeName(int streamType)
|
|
{
|
|
switch (streamType)
|
|
{
|
|
case 0x00: return "Forbidden";
|
|
case 0x01: return "ObjectDescriptorStream";
|
|
case 0x02: return "ClockReferenceStream";
|
|
case 0x03: return "SceneDescriptionStream";
|
|
case 0x04: return "VisualStream";
|
|
case 0x05: return "AudioStream";
|
|
case 0x06: return "MPEG7Stream";
|
|
case 0x07: return "IPMPStream";
|
|
case 0x08: return "ObjectContentInfoStream";
|
|
case 0x09: return "MPEGJStream";
|
|
default:
|
|
if (streamType >= 0x20 && streamType <= 0x3F)
|
|
{
|
|
return "User private";
|
|
} else
|
|
{
|
|
return "Reserved for ISO use";
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Decodes ISO/IEC 14496 MP4 object type to ASCII string
|
|
//
|
|
static const char *GetMP4ObjectTypeName(int objectTypeIndication)
|
|
{
|
|
switch (objectTypeIndication)
|
|
{
|
|
case 0x00: return "Forbidden";
|
|
case 0x01: return "Systems ISO/IEC 14496-1";
|
|
case 0x02: return "Systems ISO/IEC 14496-1";
|
|
case 0x20: return "Visual ISO/IEC 14496-2";
|
|
case 0x40: return "Audio ISO/IEC 14496-3";
|
|
case 0x60: return "Visual ISO/IEC 13818-2 Simple Profile";
|
|
case 0x61: return "Visual ISO/IEC 13818-2 Main Profile";
|
|
case 0x62: return "Visual ISO/IEC 13818-2 SNR Profile";
|
|
case 0x63: return "Visual ISO/IEC 13818-2 Spatial Profile";
|
|
case 0x64: return "Visual ISO/IEC 13818-2 High Profile";
|
|
case 0x65: return "Visual ISO/IEC 13818-2 422 Profile";
|
|
case 0x66: return "Audio ISO/IEC 13818-7 Main Profile";
|
|
case 0x67: return "Audio ISO/IEC 13818-7 LC Profile";
|
|
case 0x68: return "Audio ISO/IEC 13818-7 SSR Profile";
|
|
case 0x69: return "Audio ISO/IEC 13818-3";
|
|
case 0x6A: return "Visual ISO/IEC 11172-2";
|
|
case 0x6B: return "Audio ISO/IEC 11172-3";
|
|
case 0x6C: return "Visual ISO/IEC 10918-1";
|
|
case 0xFF: return "no object type specified";
|
|
default:
|
|
if (objectTypeIndication >= 0xC0 && objectTypeIndication <= 0xFE)
|
|
return "User private";
|
|
else
|
|
return "Reserved for ISO use";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Print MP4 information to stdout.
|
|
* Subject for customization to particular application
|
|
|
|
Output Example #1: movie file
|
|
|
|
MP4 FILE: 7 tracks found. Movie time 104.12 sec
|
|
|
|
No|type|lng| duration | bitrate| Stream type | Object type
|
|
0|odsm|fre| 0.00 s 1 frm| 0| Forbidden | Forbidden
|
|
1|sdsm|fre| 0.00 s 1 frm| 0| Forbidden | Forbidden
|
|
2|vide|```| 104.12 s 2603 frm| 1960559| VisualStream | Visual ISO/IEC 14496-2 - 720x304
|
|
3|soun|ger| 104.06 s 2439 frm| 191242| AudioStream | Audio ISO/IEC 14496-3 - 6 ch 24000 hz
|
|
4|soun|eng| 104.06 s 2439 frm| 194171| AudioStream | Audio ISO/IEC 14496-3 - 6 ch 24000 hz
|
|
5|subp|ger| 71.08 s 25 frm| 0| Forbidden | Forbidden
|
|
6|subp|eng| 71.08 s 25 frm| 0| Forbidden | Forbidden
|
|
|
|
Output Example #2: audio file with tags
|
|
|
|
MP4 FILE: 1 tracks found. Movie time 92.42 sec
|
|
title = 86-Second Blowout
|
|
artist = Yo La Tengo
|
|
album = May I Sing With Me
|
|
year = 1992
|
|
|
|
No|type|lng| duration | bitrate| Stream type | Object type
|
|
0|mdir|und| 92.42 s 3980 frm| 128000| AudioStream | Audio ISO/IEC 14496-3MP4 FILE: 1 tracks found. Movie time 92.42 sec
|
|
|
|
*/
|
|
void MP4D_printf_info(const MP4D_demux_t *mp4)
|
|
{
|
|
unsigned i;
|
|
printf("\nMP4 FILE: %d tracks found. Movie time %.2f sec\n", mp4->track_count, (4294967296.0*mp4->duration_hi + mp4->duration_lo) / mp4->timescale);
|
|
#define STR_TAG(name) if (mp4->tag.name) printf("%10s = %s\n", #name, mp4->tag.name)
|
|
STR_TAG(title);
|
|
STR_TAG(artist);
|
|
STR_TAG(album);
|
|
STR_TAG(year);
|
|
STR_TAG(comment);
|
|
STR_TAG(genre);
|
|
printf("\nNo|type|lng| duration | bitrate| %-23s| Object type", "Stream type");
|
|
for (i = 0; i < mp4->track_count; i++)
|
|
{
|
|
MP4D_track_t *tr = mp4->track + i;
|
|
|
|
printf("\n%2d|%c%c%c%c|%c%c%c|%7.2f s %6d frm| %7d|", i,
|
|
(tr->handler_type >> 24), (tr->handler_type >> 16), (tr->handler_type >> 8), (tr->handler_type >> 0),
|
|
tr->language[0], tr->language[1], tr->language[2],
|
|
(65536.0*65536.0*tr->duration_hi + tr->duration_lo) / tr->timescale,
|
|
tr->sample_count,
|
|
tr->avg_bitrate_bps);
|
|
|
|
printf(" %-23s|", GetMP4StreamTypeName(tr->stream_type));
|
|
printf(" %-23s", GetMP4ObjectTypeName(tr->object_type_indication));
|
|
|
|
if (tr->handler_type == MP4D_HANDLER_TYPE_SOUN)
|
|
{
|
|
printf(" - %d ch %d hz", tr->SampleDescription.audio.channelcount, tr->SampleDescription.audio.samplerate_hz);
|
|
} else if (tr->handler_type == MP4D_HANDLER_TYPE_VIDE)
|
|
{
|
|
printf(" - %dx%d", tr->SampleDescription.video.width, tr->SampleDescription.video.height);
|
|
}
|
|
}
|
|
printf("\n");
|
|
}
|
|
|
|
#endif // MP4D_PRINT_INFO_SUPPORTED
|
|
#endif |