diff --git a/gsmenu.sh b/gsmenu.sh index 44d0f00d..02c8ae10 100755 --- a/gsmenu.sh +++ b/gsmenu.sh @@ -195,6 +195,17 @@ case "$@" in echo 0 emit_values "0 1" ;; + "get air camera audio_enabled") + echo 0 + ;; + "get air camera audio_volume") + echo 50 + emit_values "0 100" + ;; + "get air camera audio_srate") + echo 8000 + emit_values "8000\n16000\n32000\n48000" + ;; "set air camera mirror"*) : ;; "set air camera flip"*) : ;; @@ -217,6 +228,9 @@ case "$@" in "set air camera sensor_file"*) : ;; "set air camera fpv_enable"*) : ;; "set air camera noiselevel"*) : ;; + "set air camera audio_enabled"*) : ;; + "set air camera audio_volume"*) : ;; + "set air camera audio_srate"*) : ;; # ── Air: Telemetry ─────────────────────────────────────────────────────────── @@ -397,10 +411,6 @@ case "$@" in "get gs system gs_live_colortrans") echo 0 ;; - "get gs system rec_fps") - echo 60 - emit_values "60\n90\n120" - ;; "get gs system dvr_mode"*) echo "raw" emit_values "raw\nreencode\nboth" @@ -431,6 +441,17 @@ case "$@" in "get gs system dvr_osd"*) echo 0 ;; + "get gs system audio_device"*) + echo default + emit_values "default\nrockchiphdmi\nHEADSET" + ;; + "get gs system audio_volume"*) + echo 100 + emit_values "0 100" + ;; + "get gs system audio"*) + echo 0 + ;; "set gs system rx_codec"*) : ;; "set gs system rx_mode"*) : ;; @@ -439,7 +460,6 @@ case "$@" in "set gs system resolution"*) : ;; "set gs system video_scale"*) : ;; "set gs system gs_live_colortrans"*) : ;; - "set gs system rec_fps"*) : ;; "set gs system rec_enabled"*) : ;; "set gs system dvr_mode"*) : ;; "set gs system dvr_max_size"*) : ;; # needs division by 100 @@ -448,6 +468,9 @@ case "$@" in "set gs system dvr_reenc_fps"*) : ;; "set gs system dvr_reenc_bitrate"*) : ;; "set gs system dvr_osd"*) : ;; + "set gs system audio_device"*) : ;; + "set gs system audio_volume"*) : ;; + "set gs system audio"*) : ;; # ── GS: APFPV ─────────────────────────────────────────────────────────────── diff --git a/src/dvr.cpp b/src/dvr.cpp index 686e978a..86d859d6 100644 --- a/src/dvr.cpp +++ b/src/dvr.cpp @@ -1,517 +1,60 @@ -#include -#include -#include -#include +#include #include #include +#include +#include +#include +#include #include "spdlog/spdlog.h" #include "dvr.h" -#include "minimp4.h" - -#include "gstrtpreceiver.h" -extern "C" { -#include "osd.h" -} namespace fs = std::filesystem; -// Strip H265 AUD (type 35), PREFIX_SEI (type 39), and SUFFIX_SEI (type 40) -// from an Annex-B bitstream before handing it to minimp4, which rejects these -// NAL types and would otherwise fail to write any frame. -static std::shared_ptr> -hevc_strip_supplemental(const uint8_t *data, size_t len) -{ - auto out = std::make_shared>(); - out->reserve(len); - const uint8_t *p = data; - const uint8_t *end = data + len; - for (;; p++) { - int nal_size; - p = find_nal_unit(p, (int)(end - p), &nal_size); - if (!nal_size) - break; - int nal_type = (p[0] >> 1) & 0x3f; - if (nal_type != 35 && nal_type != 39 && nal_type != 40) { - static const uint8_t sc[] = {0, 0, 0, 1}; - out->insert(out->end(), sc, sc + 4); - out->insert(out->end(), p, p + nal_size); - } - } - return out->empty() ? nullptr : out; -} - +// Master record flag; defined here, declared extern in dvr.h. int dvr_enabled = 0; -const int SEQUENCE_PADDING = 4; // Configurable padding for sequence numbers - -int write_callback(int64_t offset, const void *buffer, size_t size, void *token){ - dvr_write_ctx *ctx = (dvr_write_ctx*)token; - fseek(ctx->f, offset, SEEK_SET); - int ret = fwrite(buffer, 1, size, ctx->f) != size; - int64_t end = offset + (int64_t)size; - if (end > ctx->file_size) - ctx->file_size = end; - return ret; -} - -Dvr::Dvr(dvr_thread_params params) { - filename_template = params.filename_template; - mp4_fragmentation_mode = params.mp4_fragmentation_mode; - dvr_filenames_with_sequence = params.dvr_filenames_with_sequence; - video_framerate = params.video_framerate; - max_file_size = params.max_file_size; - video_frm_width = params.video_p.video_frm_width; - video_frm_height = params.video_p.video_frm_height; - codec = params.video_p.codec; - write_ctx.f = NULL; - write_ctx.file_size = 0; - mp4wr = nullptr; -} - -Dvr::~Dvr() {} - -void Dvr::frame(std::shared_ptr> frame) { - dvr_rpc rpc = { - .command = dvr_rpc::RPC_FRAME, - .frame = frame - }; - enqueue_dvr_command(rpc); -} - -void Dvr::set_video_params(uint32_t video_frm_w, - uint32_t video_frm_h, - VideoCodec new_codec) { - video_frm_width = video_frm_w; - video_frm_height = video_frm_h; - codec = new_codec; - dvr_rpc rpc = { - .command = dvr_rpc::RPC_SET_PARAMS - }; - enqueue_dvr_command(rpc); -} - -void Dvr::start_recording() { - dvr_rpc rpc = { - .command = dvr_rpc::RPC_START - }; - enqueue_dvr_command(rpc); -} - -void Dvr::stop_recording() { - dvr_rpc rpc = { - .command = dvr_rpc::RPC_STOP - }; - enqueue_dvr_command(rpc); -} - -void Dvr::set_video_framerate(int rate) { - video_framerate = rate; - spdlog::info("Changeing video framerate to {}",video_framerate); -} - -void Dvr::set_max_file_size(int64_t size) { - max_file_size = size; -} - -void Dvr::toggle_recording() { - dvr_rpc rpc = { - .command = dvr_rpc::RPC_TOGGLE - }; - enqueue_dvr_command(rpc); -}; - -void Dvr::shutdown() { - dvr_rpc rpc = { - .command = dvr_rpc::RPC_SHUTDOWN - }; - enqueue_dvr_command(rpc); -}; - -void Dvr::enqueue_dvr_command(dvr_rpc rpc) { - { - std::lock_guard lock(mtx); - dvrQueue.push(rpc); - } - cv.notify_one(); -} - -void *Dvr::__THREAD__(void *param) { - pthread_setname_np(pthread_self(), "__DVR"); - ((Dvr *)param)->loop(); - return nullptr; -} -void Dvr::loop() { - while (true) { - std::unique_lock lock(mtx); - cv.wait(lock, [this] { return !this->dvrQueue.empty(); }); - if (dvrQueue.empty()) { - break; - } - if (!dvrQueue.empty()) { - dvr_rpc rpc = dvrQueue.front(); - dvrQueue.pop(); - lock.unlock(); - switch (rpc.command) { - case dvr_rpc::RPC_SET_PARAMS: - { - SPDLOG_DEBUG("got rpc SET_PARAMS"); - if (write_ctx.f == NULL) { - break; // no open file; params are kept for the next start() - } - if (!writer_inited) { - // First valid params for this file: initialize the writer. - if (video_frm_width > 0 && video_frm_height > 0 && - codec != VideoCodec::UNKNOWN) { - init(); - } - break; - } - // Writer already initialized. A codec or resolution change - // can't be applied to an open mp4 track, so roll to a fresh - // file. Unchanged params are a no-op (re-initializing the open - // mux would corrupt the recording). - if (codec != init_codec || - video_frm_width != init_w || video_frm_height != init_h) { - spdlog::info("DVR params changed (codec/resolution); rolling to new file"); - stop(); - if (start() == 0) { - idr_request_record_start(); - if (video_frm_width > 0 && video_frm_height > 0 && - codec != VideoCodec::UNKNOWN) { - init(); - } - } - } - break; - } - case dvr_rpc::RPC_START: - { - SPDLOG_DEBUG("got rpc START"); - if (write_ctx.f != NULL) { - break; - } - if (start() == 0) { - idr_request_record_start(); - if (video_frm_width > 0 && video_frm_height > 0 && codec != VideoCodec::UNKNOWN) { - init(); - } - } - break; - } - case dvr_rpc::RPC_STOP: - { - SPDLOG_DEBUG("got rpc STOP"); - if (write_ctx.f == NULL) { - break; - } - stop(); - break; - } - case dvr_rpc::RPC_TOGGLE: - { - SPDLOG_DEBUG("got rpc TOGGLE"); - if (write_ctx.f == NULL) { - if (start() == 0) { - idr_request_record_start(); - if (video_frm_width > 0 && video_frm_height > 0 && codec != VideoCodec::UNKNOWN) { - init(); - } - } - } else { - stop(); - } - break; - } - case dvr_rpc::RPC_FRAME: - { - if (!_ready_to_write) { - break; - } - std::shared_ptr> frame = rpc.frame; - if (codec == VideoCodec::H265) { - frame = hevc_strip_supplemental(frame->data(), frame->size()); - if (!frame) break; - } - // Cache parameter sets as they arrive (they don't change) - if (!params_complete) - cache_parameter_sets(frame->data(), frame->size()); - // File splitting: split on next IDR when over size limit - if (max_file_size > 0 && write_ctx.file_size > max_file_size) { - if (!split_pending) { - split_pending = true; - if (on_start_cb) on_start_cb(); - idr_request_record_start(); - } - if (params_complete && is_idr(frame->data(), frame->size())) { - split(); - split_pending = false; - // Build and replay VPS/SPS/PPS into new writer - { - static const uint8_t sc[] = {0, 0, 0, 1}; - std::vector params; - if (!cached_vps.empty()) { - params.insert(params.end(), sc, sc + 4); - params.insert(params.end(), cached_vps.begin(), cached_vps.end()); - } - if (!cached_sps.empty()) { - params.insert(params.end(), sc, sc + 4); - params.insert(params.end(), cached_sps.begin(), cached_sps.end()); - } - if (!cached_pps.empty()) { - params.insert(params.end(), sc, sc + 4); - params.insert(params.end(), cached_pps.begin(), cached_pps.end()); - } - mp4_h26x_write_nal(mp4wr, params.data(), params.size(), 0); - } - mp4_h26x_write_nal(mp4wr, frame->data(), - frame->size(), 90000/video_framerate); - break; - } - } - mp4_h26x_write_nal(mp4wr, frame->data(), frame->size(), 90000/video_framerate); - break; - } - case dvr_rpc::RPC_SHUTDOWN: - goto end; - } - } - } -end: - if (write_ctx.f != NULL) { - stop(); - } - spdlog::info("DVR thread done."); -} +const int SEQUENCE_PADDING = 4; // zero-padding width for --dvr-sequenced-files -int Dvr::start() { - char *fname_tpl = filename_template; - std::string rec_dir, filename_pattern; +std::string dvr_next_base_path(const char* filename_template, bool with_sequence) { + if (!filename_template) return ""; fs::path pathObj(filename_template); - rec_dir = pathObj.parent_path().string(); - filename_pattern = pathObj.filename().string(); - std::string paddedNumber = ""; + std::string rec_dir = pathObj.parent_path().string(); + std::string filename_pattern = pathObj.filename().string(); + if (rec_dir.empty()) rec_dir = "."; - // Ensure the directory exists - if (!fs::exists(rec_dir)) - { + if (!fs::exists(rec_dir)) { spdlog::error("Error: Directory does not exist: {}", rec_dir); - return -1; + return ""; } - if (dvr_filenames_with_sequence) { - // Get the next file number - std::regex pattern(R"(^(\d+)_.*)"); // Matches filenames that start with digits followed by '_' + std::string paddedNumber; + if (with_sequence) { + std::regex pattern(R"(^(\d+)_.*)"); // filenames starting with digits then '_' int maxNumber = -1; - int nextFileNumber = 0; - for (const auto &entry : fs::directory_iterator(rec_dir)) { - if (entry.is_regular_file()) - { - std::string filename = entry.path().filename().string(); - std::smatch match; - - if (std::regex_match(filename, match, pattern)) - { - int number = std::stoi(match[1].str()); - maxNumber = std::max(maxNumber, number); - } + if (!entry.is_regular_file()) continue; + std::string filename = entry.path().filename().string(); + std::smatch match; + if (std::regex_match(filename, match, pattern)) { + int number = std::stoi(match[1].str()); + maxNumber = std::max(maxNumber, number); } } - if (maxNumber == -1) { - nextFileNumber = 0; - } else { - nextFileNumber = maxNumber + 1; - } - - // Zero-pad the number + int nextFileNumber = (maxNumber == -1) ? 0 : maxNumber + 1; std::ostringstream stream; stream << std::setw(SEQUENCE_PADDING) << std::setfill('0') << nextFileNumber; paddedNumber = stream.str() + "_"; } - // Generate timestamped filename std::time_t now = std::time(nullptr); std::tm *localTime = std::localtime(&now); - char formattedFilename[256]; std::strftime(formattedFilename, sizeof(formattedFilename), filename_pattern.c_str(), localTime); - // Construct final filename std::string finalFilename = rec_dir + "/" + paddedNumber + formattedFilename; - - // Store base path (without extension) for split naming - split_part = 0; - if (finalFilename.size() >= 4 && finalFilename.substr(finalFilename.size()-4) == ".mp4") { - current_base_path = finalFilename.substr(0, finalFilename.size()-4); - } else { - current_base_path = finalFilename; - } - - if ((write_ctx.f = fopen(finalFilename.c_str(), "w")) == NULL) { - spdlog::error("unable to open DVR file {}", finalFilename); - return -1; - } - write_ctx.file_size = 0; - mux = MP4E_open(0 /*sequential_mode*/, mp4_fragmentation_mode, &write_ctx, write_callback); - // Fresh file: writer not yet initialized and parameter sets must be - // recached for whatever codec this file ends up carrying. - writer_inited = false; - cached_vps.clear(); - cached_sps.clear(); - cached_pps.clear(); - params_complete = false; - if (max_file_size > 0) - spdlog::info("DVR file splitting enabled at {} MB", max_file_size / (1024*1024)); - return 0; -} - -void Dvr::init() { - spdlog::info("setting up dvr and mux to {}x{}", video_frm_width, video_frm_height); - if (!mp4wr) - mp4wr = (mp4_h26x_writer_t *)malloc(sizeof(mp4_h26x_writer_t)); - if (MP4E_STATUS_OK != mp4_h26x_write_init(mp4wr, mux, - video_frm_width, - video_frm_height, - codec==VideoCodec::H265)) { - spdlog::error("mp4_h26x_write_init failed"); - mux = NULL; - write_ctx.f = NULL; - writer_inited = false; - } else { - writer_inited = true; - init_codec = codec; - init_w = video_frm_width; - init_h = video_frm_height; - } - _ready_to_write = 1; - if (on_start_cb) on_start_cb(); -} - -void Dvr::stop() { - MP4E_close(mux); - mux = nullptr; - if (mp4wr) { - mp4_h26x_write_close(mp4wr); // frees the struct (minimp4 API) - mp4wr = nullptr; - } - fclose(write_ctx.f); - write_ctx.f = NULL; - write_ctx.file_size = 0; - _ready_to_write = 0; - writer_inited = false; -} - -// Check if buffer contains an IDR/IRAP slice. -bool Dvr::is_idr(const uint8_t *data, size_t len) { - const uint8_t *p = data; - const uint8_t *end = data + len; - - for (;; p++) { - int nal_size; - p = find_nal_unit(p, (int)(end - p), &nal_size); - if (!nal_size) break; - - if (codec == VideoCodec::H265) { - int nal_type = (p[0] >> 1) & 0x3f; - if (nal_type >= 16 && nal_type <= 23) return true; // IRAP - } else { - int nal_type = p[0] & 0x1f; - if (nal_type == 5) return true; // IDR - } - } - return false; -} - -// Cache VPS/SPS/PPS NALs individually from any buffer. -// Called on each frame until all required params are collected. -void Dvr::cache_parameter_sets(const uint8_t *data, size_t len) { - const uint8_t *p = data; - const uint8_t *end = data + len; - - for (;; p++) { - int nal_size; - p = find_nal_unit(p, (int)(end - p), &nal_size); - if (!nal_size) break; - - if (codec == VideoCodec::H265) { - int t = (p[0] >> 1) & 0x3f; - if (t == 32 && cached_vps.empty()) - cached_vps.assign(p, p + nal_size); - else if (t == 33 && cached_sps.empty()) - cached_sps.assign(p, p + nal_size); - else if (t == 34 && cached_pps.empty()) - cached_pps.assign(p, p + nal_size); - } else { - int t = p[0] & 0x1f; - if (t == 7 && cached_sps.empty()) - cached_sps.assign(p, p + nal_size); - else if (t == 8 && cached_pps.empty()) - cached_pps.assign(p, p + nal_size); - } - } - - if (codec == VideoCodec::H265) - params_complete = !cached_vps.empty() && !cached_sps.empty() && !cached_pps.empty(); - else - params_complete = !cached_sps.empty() && !cached_pps.empty(); -} - -void Dvr::split() { - spdlog::info("DVR file split at {} MB (part {})", - write_ctx.file_size / (1024*1024), split_part + 1); - - // Close current file - MP4E_close(mux); - mux = nullptr; - mp4_h26x_write_close(mp4wr); - mp4wr = nullptr; - fclose(write_ctx.f); - write_ctx.f = NULL; - write_ctx.file_size = 0; - - // Open next part - split_part++; - std::string nextFilename = current_base_path + "_part" + std::to_string(split_part + 1) + ".mp4"; - if ((write_ctx.f = fopen(nextFilename.c_str(), "w")) == NULL) { - spdlog::error("unable to open DVR split file {}", nextFilename); - _ready_to_write = 0; - return; - } - mux = MP4E_open(0, mp4_fragmentation_mode, &write_ctx, write_callback); - mp4wr = (mp4_h26x_writer_t *)malloc(sizeof(mp4_h26x_writer_t)); - if (MP4E_STATUS_OK != mp4_h26x_write_init(mp4wr, mux, - video_frm_width, video_frm_height, codec == VideoCodec::H265)) { - spdlog::error("mp4_h26x_write_init failed on split file"); - _ready_to_write = 0; - writer_inited = false; - } else { - writer_inited = true; - init_codec = codec; - init_w = video_frm_width; - init_h = video_frm_height; - } -} - - -// C-compatible interface -extern "C" { - void dvr_start_recording(Dvr* dvr) { - if (dvr) { - dvr->start_recording(); - } - } - - void dvr_stop_recording(Dvr* dvr) { - if (dvr) { - dvr->stop_recording(); - } - } - - void dvr_set_video_framerate(Dvr* dvr, int f) { - if (dvr) { - dvr->set_video_framerate(f); - } - } + if (finalFilename.size() >= 4 && finalFilename.substr(finalFilename.size() - 4) == ".mp4") + return finalFilename.substr(0, finalFilename.size() - 4); + return finalFilename; } diff --git a/src/dvr.h b/src/dvr.h index 08606a00..080671c4 100644 --- a/src/dvr.h +++ b/src/dvr.h @@ -1,121 +1,22 @@ #ifndef DVR_H #define DVR_H -#include -#include -#include -#include -#include -#include -#include +#include -#include "gstrtpreceiver.h" +// DVR recording is done entirely in-pipeline by the receiver's splitmuxsink +// recorders (raw + re-encode, see gstrtpreceiver.cpp) — the old minimp4 `Dvr` +// muxer has been retired. This header retains only the small shared bits: the +// recording-mode enum, the global record flag, and the filename helper. enum DvrMode { DVR_MODE_RAW = 0, DVR_MODE_REENCODE = 1, DVR_MODE_BOTH = 2 }; -struct MP4E_mux_tag; -struct mp4_h26x_writer_tag; - -struct dvr_write_ctx { - FILE *f; - int64_t file_size; // tracks max written offset -}; - -struct video_params { - uint32_t video_frm_width; - uint32_t video_frm_height; - VideoCodec codec; -}; - -struct dvr_thread_params { - char *filename_template; - int mp4_fragmentation_mode = 0; - bool dvr_filenames_with_sequence = false; - int video_framerate = -1; - int64_t max_file_size = 0; // 0 = no limit; bytes - video_params video_p; -}; - -struct dvr_rpc { - enum { - RPC_FRAME, - RPC_STOP, - RPC_START, - RPC_TOGGLE, - RPC_SHUTDOWN, - RPC_SET_PARAMS - } command; - /* union { */ - std::shared_ptr> frame; - /* video_params params; */ - /* }; */ -}; - - +// Master record flag (1 while recording). Read by the encoder/frame pacer and +// mirrored to the receiver's recorders. extern int dvr_enabled; -class Dvr { -public: - explicit Dvr(dvr_thread_params params); - virtual ~Dvr(); - - void frame(std::shared_ptr> frame); - void set_video_params(uint32_t video_frm_width, - uint32_t video_frm_height, - VideoCodec codec); - void start_recording(); - void stop_recording(); - void set_video_framerate(int rate); - void set_max_file_size(int64_t size); - void toggle_recording(); - void shutdown(); - - static void *__THREAD__(void *context); - - std::function on_start_cb; -private: - /* void *start(); */ - /* void *stop(); */ - void enqueue_dvr_command(dvr_rpc rpc); - - void loop(); - int start(); - void stop(); - void init(); - void split(); - bool is_idr(const uint8_t *data, size_t len); - void cache_parameter_sets(const uint8_t *data, size_t len); -private: - std::queue dvrQueue; - std::mutex mtx; - std::condition_variable cv; - char *filename_template; - int mp4_fragmentation_mode = 0; - bool dvr_filenames_with_sequence = false; - int video_framerate = -1; - int64_t max_file_size = 0; - uint32_t video_frm_width; - uint32_t video_frm_height; - VideoCodec codec; - // What the currently-open mp4 writer was initialized with. A mid-recording - // codec/resolution change cannot be applied to an open mp4 track, so we roll - // to a fresh file instead of re-initializing (which corrupts the mux). - VideoCodec init_codec = VideoCodec::UNKNOWN; - uint32_t init_w = 0; - uint32_t init_h = 0; - bool writer_inited = false; - int _ready_to_write = 0; - bool split_pending = false; - int split_part = 0; - std::string current_base_path; // base path without .mp4 for split naming - std::vector cached_vps; // H.265 only - std::vector cached_sps; - std::vector cached_pps; - bool params_complete = false; - - dvr_write_ctx write_ctx; - MP4E_mux_tag *mux; - mp4_h26x_writer_tag *mp4wr; -}; +// Resolve the next DVR recording's base path from a filename template WITHOUT +// the extension: /[NNNN_] minus a trailing ".mp4". +// Honours --dvr-sequenced-files. Returns "" if the directory does not exist. +std::string dvr_next_base_path(const char* filename_template, bool with_sequence); #endif diff --git a/src/gsmenu/colmenu.c b/src/gsmenu/colmenu.c index 86c7ec74..81a437d3 100644 --- a/src/gsmenu/colmenu.c +++ b/src/gsmenu/colmenu.c @@ -14,6 +14,9 @@ extern gsmenu_control_mode_t control_mode; extern lv_indev_t * indev_drv; +extern int audio_get_enabled(void); +extern void audio_set_enabled(int enabled); + static colstack_t * g_cs; /* True while the command-error dialog is up, so async teardowns (e.g. the text @@ -94,6 +97,12 @@ char * colmenu_get(const char * type, const char * page, const char * param, cha return strdup(menu_is_recording() ? "1" : "0"); } + /* Audio on/off is live runtime state on the receiver, not a config value — + * read it straight from the app (switch builder atoi()s this: 0/1, not on/off). */ + if(strcmp(param, "audio") == 0) { + return strdup(audio_get_enabled() ? "1" : "0"); + } + char errf[] = "/tmp/gsmenu_gerr_XXXXXX"; int efd = mkstemp(errf); if(efd >= 0) close(efd); diff --git a/src/gsmenu/colmenu_pages.c b/src/gsmenu/colmenu_pages.c index bbd2971f..5fe22dd0 100644 --- a/src/gsmenu/colmenu_pages.c +++ b/src/gsmenu/colmenu_pages.c @@ -26,6 +26,11 @@ extern MenuAction airactions[]; extern size_t airactions_count; extern MenuAction gsactions[]; extern size_t gsactions_count; extern enum RXMode RXMODE; +extern int audio_get_enabled(void); +extern void audio_set_enabled(int enabled); +extern void audio_set_device(const char * device); +extern void audio_set_volume(int percent); + /* Live Colortrans / Video scale apply their effect to the DRM/GL pipeline live, * as the old gs_system.c widget callbacks did — a gsmenu.sh set alone won't. */ extern bool enable_live_colortrans; @@ -44,7 +49,6 @@ void dvr_reenc_set_codec(int idx); void dvr_reenc_set_resolution(int idx); void dvr_set_mode(int mode); void dvr_set_max_size(int mb); -void dvr_set_raw_fps(int fps); void dvr_start_all(void); void dvr_stop_all(void); #endif @@ -83,63 +87,59 @@ static void on_video_scale(const char * value) #endif } -/* ── GS DVR live hooks ─────────────────────────────────────────────────────── +/* ── GS live-apply hooks ───────────────────────────────────────────────────── * These restore the "apply immediately" behaviour the old gs_system.c widget - * callbacks had. Without them a change to a DVR setting only persists to config - * (gsmenu.sh) and takes effect on the next restart. The dropdown value strings - * come straight from gsmenu.sh's option lists. */ + * callbacks had. Without them a menu change would only persist to config + * (gsmenu.sh) and take effect on the next restart. Used by the DVR, audio and + * other receiver settings. The value strings come straight from gsmenu.sh's + * option lists. In the simulator there's no backend, so we just print the call. */ #ifdef USE_SIMULATOR -#define DVR_LIVE(call, fmt, ...) do { printf(fmt "\n", __VA_ARGS__); fflush(stdout); } while(0) +#define MENU_LIVE(call, fmt, ...) do { printf(fmt "\n", __VA_ARGS__); fflush(stdout); } while(0) #else -#define DVR_LIVE(call, fmt, ...) do { call; } while(0) +#define MENU_LIVE(call, fmt, ...) do { call; } while(0) #endif static void on_rec_enabled(const char * value) /* start/stop recording now */ { int on = value && strcmp(value, "on") == 0; - if(on) DVR_LIVE(dvr_start_all(), "dvr_start_all()%s", ""); - else DVR_LIVE(dvr_stop_all(), "dvr_stop_all()%s", ""); + if(on) MENU_LIVE(dvr_start_all(), "dvr_start_all()%s", ""); + else MENU_LIVE(dvr_stop_all(), "dvr_stop_all()%s", ""); } static void on_dvr_osd(const char * value) /* burn OSD into the re-encode */ { int on = (value && strcmp(value, "on") == 0) ? 1 : 0; - DVR_LIVE(dvr_reenc_set_osd(on), "dvr_reenc_set_osd(%d)", on); + MENU_LIVE(dvr_reenc_set_osd(on), "dvr_reenc_set_osd(%d)", on); } static void on_dvr_mode(const char * value) /* raw=0, reencode=1, both=2 */ { int mode = 0; if(value) { if(!strcmp(value, "reencode")) mode = 1; else if(!strcmp(value, "both")) mode = 2; } - DVR_LIVE(dvr_set_mode(mode), "dvr_set_mode(%d)", mode); + MENU_LIVE(dvr_set_mode(mode), "dvr_set_mode(%d)", mode); } static void on_dvr_max_size(const char * value) /* param is in units of 100 MB */ { int mb = (value ? atoi(value) : 0) * 100; - DVR_LIVE(dvr_set_max_size(mb), "dvr_set_max_size(%d)", mb); -} -static void on_rec_fps(const char * value) /* raw recorder framerate */ -{ - int fps = value ? atoi(value) : 0; - if(fps > 0) DVR_LIVE(dvr_set_raw_fps(fps), "dvr_set_raw_fps(%d)", fps); + MENU_LIVE(dvr_set_max_size(mb), "dvr_set_max_size(%d)", mb); } static void on_dvr_reenc_codec(const char * value) /* h264=0, h265=1 */ { int idx = (value && strcmp(value, "h265") == 0) ? 1 : 0; - DVR_LIVE(dvr_reenc_set_codec(idx), "dvr_reenc_set_codec(%d)", idx); + MENU_LIVE(dvr_reenc_set_codec(idx), "dvr_reenc_set_codec(%d)", idx); } static void on_dvr_reenc_resolution(const char * value) /* 720p=0, 1080p=1 */ { int idx = (value && strcmp(value, "1080p") == 0) ? 1 : 0; - DVR_LIVE(dvr_reenc_set_resolution(idx), "dvr_reenc_set_resolution(%d)", idx); + MENU_LIVE(dvr_reenc_set_resolution(idx), "dvr_reenc_set_resolution(%d)", idx); } static void on_dvr_reenc_fps(const char * value) { int fps = value ? atoi(value) : 0; - if(fps > 0) DVR_LIVE(dvr_reenc_set_fps(fps), "dvr_reenc_set_fps(%d)", fps); + if(fps > 0) MENU_LIVE(dvr_reenc_set_fps(fps), "dvr_reenc_set_fps(%d)", fps); } static void on_dvr_reenc_bitrate(const char * value) { int kbps = value ? atoi(value) : 0; - if(kbps > 0) DVR_LIVE(dvr_reenc_set_bitrate(kbps), "dvr_reenc_set_bitrate(%d)", kbps); + if(kbps > 0) MENU_LIVE(dvr_reenc_set_bitrate(kbps), "dvr_reenc_set_bitrate(%d)", kbps); } /* Full-screen editors reached from the menu (built by the app elsewhere). Each @@ -172,6 +172,22 @@ static void open_txprofiles(void) static void notify_restart(const char * v) { (void)v; show_restart_notice(); } +static void on_audio_enabled(const char * value) /* toggle Opus audio playback now */ +{ + int on = (value && strcmp(value, "on") == 0) ? 1 : 0; + MENU_LIVE(audio_set_enabled(on), "audio_set_enabled(%d)", on); +} +static void on_audio_device(const char * value) /* switch the ALSA output card now */ +{ + const char * dev = value ? value : ""; + MENU_LIVE(audio_set_device(dev), "audio_set_device(%s)", dev); +} +static void on_audio_volume(const char * value) /* software output volume 0-100% */ +{ + int pct = value ? atoi(value) : 100; + MENU_LIVE(audio_set_volume(pct), "audio_set_volume(%d)", pct); +} + /* Apply the receiver mode: set RXMODE and the env vars the rest of the app reads * (REMOTE_IP / AIR_FIRMWARE_TYPE). Called both at startup and on a mode change, so * the env is always in sync with the actual mode (the old gsmenu_toggle_rxmode() @@ -295,14 +311,23 @@ static const colmenu_item_t cam_fpv_items[] = { { .kind=COLMENU_SLIDER, .label="Noiselevel", .param="noiselevel" }, }; static const colmenu_page_t cam_fpv_page = { "FPV", "air", "camera", cam_fpv_items, 2 }; +/* Air-unit audio (majestic.yaml audio.*). Applied on the air side via gsmenu.sh + * (cli -s + majestic restart); no local hooks needed. */ +static const colmenu_item_t cam_audio_items[] = { + { .kind=COLMENU_SWITCH, .label="Enabled", .param="audio_enabled" }, + { .kind=COLMENU_SLIDER, .label="Volume", .param="audio_volume" }, + { .kind=COLMENU_DROPDOWN, .label="Sample Rate", .param="audio_srate" }, +}; +static const colmenu_page_t cam_audio_page = { "Audio", "air", "camera", cam_audio_items, 3 }; static const colmenu_item_t camera_items[] = { { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_VIDEO, .label="Video", .sub=&cam_video_page }, { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_IMAGE, .label="Image", .sub=&cam_image_page }, { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_VIDEO, .label="Recording", .sub=&cam_rec_page }, { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_EYE_OPEN, .label="ISP", .sub=&cam_isp_page }, { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_GPS, .label="FPV", .sub=&cam_fpv_page }, + { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_AUDIO, .label="Audio", .sub=&cam_audio_page }, }; -static const colmenu_page_t camera_page = { "Camera", "air", "camera", camera_items, 5 }; +static const colmenu_page_t camera_page = { "Camera", "air", "camera", camera_items, 6 }; static const colmenu_item_t air_tel_items[] = { { .kind=COLMENU_DROPDOWN, .label="Serial Port", .param="serial" }, @@ -376,6 +401,12 @@ static const colmenu_item_t sys_receiver_items[] = { { .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_SETTINGS, .label="RX Mode", .param="rx_mode", .on_change=on_rx_mode_change }, }; static const colmenu_page_t sys_receiver_page = { "Receiver", "gs", "system", sys_receiver_items, 2 }; +static const colmenu_item_t sys_audio_items[] = { + { .kind=COLMENU_SWITCH, .icon=LV_SYMBOL_AUDIO, .label="Audio", .param="audio", .on_change=on_audio_enabled }, + { .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_AUDIO, .label="Output", .param="audio_device", .on_change=on_audio_device }, + { .kind=COLMENU_SLIDER, .icon=LV_SYMBOL_VOLUME_MAX, .label="Volume", .param="audio_volume", .on_change=on_audio_volume, .on_live=on_audio_volume }, +}; +static const colmenu_page_t sys_audio_page = { "Audio", "gs", "system", sys_audio_items, 3 }; static const colmenu_item_t sys_display_items[] = { { .kind=COLMENU_SWITCH, .icon=LV_SYMBOL_SETTINGS, .label="GS Rendering", .param="gs_rendering" }, { .kind=COLMENU_DROPDOWN, .icon=LV_SYMBOL_SETTINGS, .label="Connector", .param="connector" }, @@ -388,20 +419,20 @@ static const colmenu_item_t sys_dvr_items[] = { { .kind=COLMENU_SWITCH, .label="Enabled", .param="rec_enabled", .on_change=on_rec_enabled }, { .kind=COLMENU_DROPDOWN, .label="Mode", .param="dvr_mode", .on_change=on_dvr_mode }, { .kind=COLMENU_SLIDER, .label="Max file size (MB)", .param="dvr_max_size", .on_change=on_dvr_max_size, .display_scale=100 }, - { .kind=COLMENU_DROPDOWN, .label="Raw FPS", .param="rec_fps", .on_change=on_rec_fps }, { .kind=COLMENU_DROPDOWN, .label="Codec", .param="dvr_reenc_codec", .on_change=on_dvr_reenc_codec }, { .kind=COLMENU_DROPDOWN, .label="Resolution", .param="dvr_reenc_resolution", .on_change=on_dvr_reenc_resolution }, { .kind=COLMENU_DROPDOWN, .label="Re-encode FPS", .param="dvr_reenc_fps", .on_change=on_dvr_reenc_fps }, { .kind=COLMENU_DROPDOWN, .label="Bitrate (kbps)", .param="dvr_reenc_bitrate", .on_change=on_dvr_reenc_bitrate }, { .kind=COLMENU_SWITCH, .label="Record OSD in DVR", .param="dvr_osd", .on_change=on_dvr_osd }, }; -static const colmenu_page_t sys_dvr_page = { "DVR", "gs", "system", sys_dvr_items, 9 }; +static const colmenu_page_t sys_dvr_page = { "DVR", "gs", "system", sys_dvr_items, 8 }; static const colmenu_item_t system_items[] = { { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_WIFI, .label="Receiver", .sub=&sys_receiver_page }, + { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_AUDIO, .label="Audio", .sub=&sys_audio_page }, { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_IMAGE, .label="Display", .sub=&sys_display_page }, { .kind=COLMENU_SUBMENU, .icon=LV_SYMBOL_VIDEO, .label="DVR", .sub=&sys_dvr_page }, }; -static const colmenu_page_t system_page = { "System", "gs", "system", system_items, 3 }; +static const colmenu_page_t system_page = { "System", "gs", "system", system_items, 4 }; /* WiFi. The WiFi page shows the live connection (get gs wifi ssid) — entering the * connected network gives Disconnect / Forget. "Networks" lists only AVAILABLE diff --git a/src/gstrtpreceiver.cpp b/src/gstrtpreceiver.cpp index 9fbeca72..ae4de09a 100644 --- a/src/gstrtpreceiver.cpp +++ b/src/gstrtpreceiver.cpp @@ -48,12 +48,70 @@ namespace pipeline { } return ss.str(); } - static std::string create_rtp_depacketize_for_codec(const VideoCodec& codec){ - if(codec==VideoCodec::H264)return "rtph264depay ! "; - if(codec==VideoCodec::H265)return "rtph265depay ! "; + static std::string create_rtp_depacketize_for_codec(const VideoCodec& codec, const std::string& name = ""){ + const std::string n = name.empty() ? "" : (" name=" + name); + if(codec==VideoCodec::H264)return "rtph264depay" + n + " ! "; + if(codec==VideoCodec::H265)return "rtph265depay" + n + " ! "; assert(false); return ""; } + // Bare RTP caps fields for use as an in-pipeline capsfilter on the video + // branch when the source caps are left generic (audio muxed in). Unlike + // gst_create_rtp_caps() this omits the udpsrc-style caps="..." wrapper and + // does not pin a payload type, so the actual video PT is accepted as-is. + static std::string gst_rtp_video_caps_fields(const VideoCodec& videoCodec){ + std::stringstream ss; + ss<<"application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)"; + ss<<((videoCodec==VideoCodec::H264) ? "H264" : "H265"); + return ss.str(); + } + // Opus audio playback branch, fed from the shared rtp_tee. + // + // The leading leaky queue decouples the branch from the tee so a stalled or + // slow ALSA sink can never back-pressure upstream and stall the video branch. + // + // Caps are asserted with capssetter, NOT a capsfilter: the tee broadcasts one + // caps to all its branches, so a capsfilter demanding audio caps here would + // force the tee to negotiate video-caps ∩ audio-caps = empty and the whole + // pipeline (video included) would fail with not-negotiated. capssetter has an + // ANY sink template, so it imposes nothing on the tee while still handing the + // Opus RTP caps to rtpopusdepay downstream. A pad probe (attached after + // parsing) drops the non-audio-PT packets that still arrive here. + // Map the user's output selection to an alsasink "device". Empty or "default" + // -> system default (no device property). A bare ALSA card id (e.g. the + // "rockchiphdmi"/"HEADSET" ids from /proc/asound/cards, as the OSD menu + // provides) -> plughw:CARD= so format/rate conversion is handled. A value + // that already looks like a full ALSA device string (contains ':') is used + // verbatim, so power users can still pass e.g. plughw:CARD=x,DEV=1 via CLI. + static std::string resolve_alsa_device(const std::string& sel){ + if(sel.empty() || sel == "default") return ""; + if(sel.find(':') != std::string::npos) return sel; + return "plughw:CARD=" + sel + ",DEV=0"; + } + + static std::string create_audio_branch(int audio_pt, const std::string& device){ + std::stringstream ss; + ss<<" rtp_tee. ! queue name=audio_in_queue leaky=downstream max-size-buffers=128" + " max-size-bytes=0 max-size-time=0 silent=true" + " ! capssetter replace=true caps=\"application/x-rtp, media=(string)audio," + " clock-rate=(int)48000, encoding-name=(string)OPUS, payload=(int)"< g_codec_switch_cb; + // RTP payload type carrying muxed Opus audio, or -1 when audio is disabled. + // Used to keep audio packets out of the video-only stream trackers (IDR + // sequence-gap detection and mid-stream codec-switch detection). + static std::atomic g_audio_pt{-1}; + // Wall-clock ms of the last observed audio (Opus) packet; 0 = never seen. + // Lets the DVR decide whether a recording should carry an audio track. + static std::atomic g_last_audio_pkt_ms{0}; + + static bool is_audio_pt(uint8_t pt) { + const int a = g_audio_pt.load(std::memory_order_relaxed); + return a >= 0 && pt == static_cast(a); + } + static std::mutex g_idr_sock_mutex; static int g_idr_sock = -1; static std::atomic g_idr_sock_ready{false}; @@ -183,6 +254,16 @@ namespace { return std::chrono::duration_cast(now).count(); } + // True if Opus is flowing right now (a packet seen very recently). Recording + // an empty audio track — which happens when --audio is on but the air sends + // no audio — produces an unplayable mp4, so the DVR only adds an audio track + // when this is true. Kept short so a recording started shortly after audio + // stops doesn't get an empty track either. + static bool audio_recently_seen() { + const uint64_t last = g_last_audio_pkt_ms.load(std::memory_order_relaxed); + return last != 0 && (now_ms() - last) < 1500; + } + static void request_idr_bursts(const char* reason, int request_count, bool allow_pending); static void maybe_update_restream_target(bool force); @@ -486,6 +567,19 @@ namespace { return; } + // Muxed audio has its own SSRC/sequence space; feeding it into the video + // gap detector would trip spurious IDR requests, so skip audio packets. + if (g_audio_pt.load(std::memory_order_relaxed) >= 0) { + GstMapInfo map; + if (gst_buffer_map(buf, &map, GST_MAP_READ)) { + const bool audio = map.size >= 2 && is_audio_pt(map.data[1] & 0x7f); + gst_buffer_unmap(buf, &map); + if (audio) { + return; + } + } + } + uint16_t seq = 0; if (!extract_rtp_sequence(buf, &seq)) { return; @@ -547,6 +641,9 @@ namespace { if (g_codec_switch_pending.load(std::memory_order_relaxed)) { return; // a switch is already being applied } + if (len >= 2 && is_audio_pt(rtp[1] & 0x7f)) { + return; // muxed audio packet: not a video codec signal + } const VideoCodec c = classify_rtp_packet(rtp, len); if (c == VideoCodec::UNKNOWN) { @@ -919,6 +1016,108 @@ namespace { gst_iterator_free(it); } + // Payload-type demux for the shared RTP flow. When audio is muxed in, the + // tee hands every packet to both the video and audio branches; these probes + // drop the packets that do not belong on a given branch before they reach a + // depayloader that would choke on them. drop_when_match=true keeps the audio + // PT off the video branch; false keeps everything but the audio PT off the + // audio branch. + struct RtpPtFilter { + uint8_t pt; + bool drop_when_match; + }; + + static GstPadProbeReturn rtp_pt_filter_probe(GstPad*, GstPadProbeInfo* info, gpointer user_data) { + const RtpPtFilter* f = static_cast(user_data); + if (!f || !(GST_PAD_PROBE_INFO_TYPE(info) & GST_PAD_PROBE_TYPE_BUFFER)) { + return GST_PAD_PROBE_OK; + } + GstBuffer* buf = GST_PAD_PROBE_INFO_BUFFER(info); + if (!buf) { + return GST_PAD_PROBE_OK; + } + bool drop = false; + GstMapInfo map; + if (gst_buffer_map(buf, &map, GST_MAP_READ)) { + if (map.size >= 2) { + const bool match = ((map.data[1] & 0x7f) == f->pt); + // f->pt is the audio PT for both branch filters, so a match means + // an Opus packet just went by — note it for audio_recently_seen(). + if (match) g_last_audio_pkt_ms.store(now_ms(), std::memory_order_relaxed); + drop = f->drop_when_match ? match : !match; + } + gst_buffer_unmap(buf, &map); + } + return drop ? GST_PAD_PROBE_DROP : GST_PAD_PROBE_OK; + } + + static void attach_pt_filter(GstElement* pipeline, const char* elem_name, + uint8_t pt, bool drop_when_match) { + if (!pipeline || !GST_IS_BIN(pipeline)) { + return; + } + GstElement* e = gst_bin_get_by_name(GST_BIN(pipeline), elem_name); + if (!e) { + return; + } + GstPad* pad = gst_element_get_static_pad(e, "sink"); + if (pad) { + RtpPtFilter* f = g_new(RtpPtFilter, 1); + f->pt = pt; + f->drop_when_match = drop_when_match; + gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_BUFFER, rtp_pt_filter_probe, f, + (GDestroyNotify)g_free); + gst_object_unref(pad); + } + gst_object_unref(e); + } + + static bool audio_factory_exists(const char* name) { + GstElementFactory* f = gst_element_factory_find(name); + if (f) { + gst_object_unref(f); + return true; + } + spdlog::warn("[AUDIO] Missing GStreamer element '{}'; disabling audio", name); + return false; + } + + // Verify the Opus/ALSA elements exist and the resolved output device can + // actually be opened, so audio can gracefully fall back to video-only instead + // of failing the whole pipeline (which would also kill video). Important for + // hot-pluggable sinks (e.g. a USB headset): if the selected card is gone the + // audio branch is simply left out until the next rebuild finds it available. + // `device` is the resolved alsasink device string ("" = system default). + static bool audio_stack_available(const std::string& device) { + static const char* kNeeded[] = { + "capssetter", "rtpopusdepay", "opusdec", "audioconvert", "audioresample", "volume", "alsasink" + }; + for (const char* name : kNeeded) { + if (!audio_factory_exists(name)) { + return false; + } + } + + GstElement* sink = gst_element_factory_make("alsasink", nullptr); + if (!sink) { + return false; + } + if (!device.empty()) { + g_object_set(G_OBJECT(sink), "device", device.c_str(), NULL); + } + // NULL -> READY opens the PCM device; a failure here means the device is + // absent or busy, so keep audio off rather than break the pipeline. + const GstStateChangeReturn r = gst_element_set_state(sink, GST_STATE_READY); + const bool ok = (r != GST_STATE_CHANGE_FAILURE); + gst_element_set_state(sink, GST_STATE_NULL); + gst_object_unref(sink); + if (!ok) { + spdlog::warn("[AUDIO] ALSA device '{}' unavailable; disabling audio", + device.empty() ? "default" : device); + } + return ok; + } + static void maybe_request_idr_rate_limited(const char* reason, const char* context) { if (!g_idr_enabled.load(std::memory_order_relaxed)) { return; @@ -1056,7 +1255,8 @@ static std::shared_ptr> gst_copy_buffer(GstBuffer* buffer){ } static void loop_pull_appsink_samples(bool& keep_looping,GstElement *app_sink_element, - const GstRtpReceiver::NEW_FRAME_CALLBACK out_cb){ + const GstRtpReceiver::NEW_FRAME_CALLBACK out_cb, + const std::function& on_tick){ assert(app_sink_element); assert(out_cb); const uint64_t timeout_ns=std::chrono::duration_cast(std::chrono::milliseconds(100)).count(); @@ -1075,6 +1275,10 @@ static void loop_pull_appsink_samples(bool& keep_looping,GstElement *app_sink_el } maybe_update_restream_target(false); tick_stream_presence(); + // Drive the DVR record branch here (not from the menu/signal threads): + // this thread's lifetime is bounded by the pipeline's, so add/remove of + // the splitmuxsink branch is always against the live pipeline. + if (on_tick) on_tick(); } } @@ -1082,15 +1286,31 @@ static void loop_pull_appsink_samples(bool& keep_looping,GstElement *app_sink_el std::string GstRtpReceiver::construct_gstreamer_pipeline() { std::stringstream ss; + // With audio muxed into the same RTP flow the source caps must stay generic + // (application/x-rtp) so both the video and audio branches can negotiate + // their own media type off the tee; the video branch then re-asserts its + // caps with an explicit capsfilter. When audio is off, the source keeps the + // codec-specific caps exactly as before. + const std::string src_caps = m_audio_active + ? std::string("caps=\"application/x-rtp\"") + : pipeline::gst_create_rtp_caps(m_video_codec); if (! unix_socket) - ss<<"udpsrc port="<> sample){ this->on_new_sample(sample); }; - loop_pull_appsink_samples(m_pull_samples_run,m_app_sink_element,cb); + loop_pull_appsink_samples(m_pull_samples_run,m_app_sink_element,cb, + [this]{ this->dvr_tick(); this->handle_bus_messages(); }); +} + +void GstRtpReceiver::handle_bus_messages() +{ + if (!m_gst_pipeline) return; + GstBus* bus = gst_element_get_bus(m_gst_pipeline); + if (!bus) return; + + bool audio_failed = false; + GstMessage* msg; + // Pop ERROR/WARNING only — the DVR relies on ELEMENT (fragment-closed) + // messages, which stay in the bus for dvr_remove_*_bin() to consume. Draining + // here is also what stops a failing sink's message spam from exhausting memory. + while ((msg = gst_bus_pop_filtered( + bus, (GstMessageType)(GST_MESSAGE_ERROR | GST_MESSAGE_WARNING))) != nullptr) { + GstObject* src = GST_MESSAGE_SRC(msg); + gchar* name = src ? gst_object_get_name(src) : nullptr; + const bool from_audio_sink = name && strstr(name, "audio_sink") != nullptr; + + GError* err = nullptr; + gchar* dbg = nullptr; + if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_ERROR) + gst_message_parse_error(msg, &err, &dbg); + else + gst_message_parse_warning(msg, &err, &dbg); + + if (from_audio_sink) { + spdlog::warn("[AUDIO] output sink {}: {}", + GST_MESSAGE_TYPE(msg) == GST_MESSAGE_ERROR ? "error" : "warning", + err ? err->message : "unknown"); + audio_failed = true; + } else if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_ERROR) { + spdlog::warn("[PIPE] error from {}: {}", name ? name : "?", err ? err->message : "unknown"); + } + if (err) g_error_free(err); + g_free(dbg); + g_free(name); + gst_message_unref(msg); + } + gst_object_unref(bus); + + // A dead audio sink (e.g. USB headset unplugged) would otherwise have alsasink + // spinning at 100% CPU. Rebuild video-only: switch_to_stream()'s pre-flight + // (audio_stack_available) finds the device gone and leaves audio out, which + // also tears the spinning sink down. Off-thread because switch_to_stream() + // joins this pull thread; guarded so we spawn exactly one rebuild. + if (audio_failed && m_audio_active && + !m_audio_rebuilding.exchange(true, std::memory_order_acq_rel)) { + spdlog::warn("[AUDIO] Output sink failed (device unplugged?) — falling back to video-only"); + std::thread([this]() { + switch_to_stream(); + m_audio_rebuilding.store(false, std::memory_order_release); + }).detach(); + } } void GstRtpReceiver::on_new_sample(std::shared_ptr > sample) @@ -1215,11 +1490,26 @@ void GstRtpReceiver::stop_receiving() { if (m_gst_pipeline != nullptr) { clear_restream_valve(); + // Finalize any active recording cleanly BEFORE tearing the pipeline down + // (writes each mp4's moov via EOS + fragment-closed wait). The pull thread + // is already joined, so no dvr_tick can race this. m_dvr_want is left as-is + // so a recording that spans a rebuild is re-armed on the new pipeline. + if (m_dvr_rec_bin) dvr_remove_record_bin(); + if (m_dvr_reenc_bin) dvr_remove_reenc_bin(); gst_element_send_event((GstElement*)m_gst_pipeline, gst_event_new_eos()); gst_element_set_state(m_gst_pipeline, GST_STATE_PAUSED); gst_element_set_state(m_gst_pipeline, GST_STATE_NULL); gst_object_unref(m_gst_pipeline); m_gst_pipeline = nullptr; + // Safety net: drop any handles the removes above didn't (shouldn't happen). + if (m_dvr_tee_video_pad) { gst_object_unref(m_dvr_tee_video_pad); m_dvr_tee_video_pad = nullptr; } + if (m_dvr_tee_audio_pad) { gst_object_unref(m_dvr_tee_audio_pad); m_dvr_tee_audio_pad = nullptr; } + m_dvr_rec_bin = nullptr; + m_dvr_active.store(false, std::memory_order_relaxed); + { std::lock_guard lk(m_dvr_reenc_src_mutex); m_dvr_reenc_appsrc = nullptr; } + if (m_dvr_reenc_tee_audio_pad) { gst_object_unref(m_dvr_reenc_tee_audio_pad); m_dvr_reenc_tee_audio_pad = nullptr; } + m_dvr_reenc_bin = nullptr; + m_dvr_reenc_active.store(false, std::memory_order_relaxed); } reset_stream_tracking(); spdlog::info("GstRtpReceiver::stop_receiving end"); @@ -1245,7 +1535,13 @@ std::string GstRtpReceiver::construct_file_playback_pipeline(const char * file_p } VideoCodec GstRtpReceiver::switch_to_file_playback(const char * file_path) { + std::lock_guard lock(m_stream_mutex); stop_receiving(); + m_file_playback = true; + + // File playback has no live RTP ingress; make sure the audio-PT tracker + // guard is inert so it can't affect anything during DVR review. + g_audio_pt.store(-1, std::memory_order_relaxed); const auto pipeline = construct_file_playback_pipeline(file_path); GError* error = nullptr; @@ -1276,7 +1572,9 @@ VideoCodec GstRtpReceiver::switch_to_file_playback(const char * file_path) { } void GstRtpReceiver::switch_to_stream() { + std::lock_guard lock(m_stream_mutex); stop_receiving(); + m_file_playback = false; // Auto mode: build for H.265 up front and let mid-stream detection flip to // H.264 from the RTP ingress if the stream turns out to be H.264. The @@ -1288,6 +1586,21 @@ void GstRtpReceiver::switch_to_stream() { spdlog::info("[CODEC] Auto mode: defaulting to H.265; mid-stream detection will correct if needed"); } + // Resolve the effective audio state: only build the Opus branch if the user + // wants audio AND the Opus/ALSA stack + selected output device are actually + // usable right now. Otherwise fall back to video-only so a missing plugin or + // an unplugged/absent sink can't take the whole pipeline (and thus video) + // down. The user's intent (m_audio_enabled) and selection (m_audio_device) + // are kept, so the next rebuild picks the device back up once it returns. + m_audio_active = m_audio_enabled && + audio_stack_available(pipeline::resolve_alsa_device(m_audio_device)); + g_audio_pt.store(m_audio_active ? m_audio_pt : -1, std::memory_order_relaxed); + if (m_audio_enabled) { + spdlog::info("[AUDIO] Opus audio {} (pt={}, device={})", + m_audio_active ? "enabled" : "requested but unavailable -> video-only", + m_audio_pt, m_audio_device.empty() ? "default" : m_audio_device); + } + const auto pipeline = construct_gstreamer_pipeline(); GError* error = nullptr; m_gst_pipeline = gst_parse_launch(pipeline.c_str(), &error); @@ -1308,6 +1621,21 @@ void GstRtpReceiver::switch_to_stream() { attach_last_hop_probes(m_gst_pipeline); bind_restream_valve(m_gst_pipeline); + // Payload-type demux: keep audio packets off the video depayloader and + // non-audio packets off the Opus depayloader (the tee feeds both branches + // every packet). + if (m_audio_active) { + const uint8_t audio_pt = static_cast(m_audio_pt); + attach_pt_filter(m_gst_pipeline, "video_depay", audio_pt, /*drop_when_match=*/true); + attach_pt_filter(m_gst_pipeline, "audio_depay", audio_pt, /*drop_when_match=*/false); + // Apply the configured software volume to the freshly-built branch. + GstElement* vol = gst_bin_get_by_name(GST_BIN(m_gst_pipeline), "audio_volume"); + if (vol) { + g_object_set(vol, "volume", static_cast(m_audio_volume), NULL); + gst_object_unref(vol); + } + } + // If using Unix socket, setup appsrc with buffer pool if (unix_socket) { GstElement* appsrc = gst_bin_get_by_name(GST_BIN(m_gst_pipeline), "appsrc"); @@ -1332,12 +1660,17 @@ void GstRtpReceiver::switch_to_stream() { pool = gst_buffer_pool_new(); config = gst_buffer_pool_get_config(pool); - GstCaps* caps = gst_caps_new_simple("application/x-rtp", - "media", G_TYPE_STRING, "video", - "encoding-name", G_TYPE_STRING, - (m_video_codec == VideoCodec::H264) ? "H264" : "H265", - NULL); - + // With audio muxed in, the appsrc carries mixed media so its caps stay + // generic (matching the pipeline string); the per-branch capsfilters + // pick the media type. Otherwise pin the video codec as before. + GstCaps* caps = m_audio_active + ? gst_caps_new_empty_simple("application/x-rtp") + : gst_caps_new_simple("application/x-rtp", + "media", G_TYPE_STRING, "video", + "encoding-name", G_TYPE_STRING, + (m_video_codec == VideoCodec::H264) ? "H264" : "H265", + NULL); + gst_buffer_pool_config_set_params(config, caps, MAX_PACKET_SIZE, 10, 20); gst_buffer_pool_set_config(pool, config); gst_caps_unref(caps); @@ -1393,6 +1726,65 @@ void GstRtpReceiver::request_codec_switch(VideoCodec new_codec) { }).detach(); } +void GstRtpReceiver::configure_audio(bool enabled, const std::string& device, int pt, double volume) { + m_audio_enabled = enabled; + m_audio_device = device; + m_audio_pt = (pt > 0 && pt < 128) ? pt : 98; + m_audio_volume = (volume < 0.0) ? 0.0 : (volume > 1.0 ? 1.0 : volume); +} + +void GstRtpReceiver::set_audio_volume(double volume) { + volume = (volume < 0.0) ? 0.0 : (volume > 1.0 ? 1.0 : volume); + m_audio_volume = volume; + + // Apply live to the running volume element (no rebuild needed). Serialize + // the pipeline read with switch_to_stream() so we never touch a half-swapped + // pipeline; the value is stored above regardless, for the next build. + std::lock_guard lock(m_stream_mutex); + if (!m_gst_pipeline) { + return; + } + GstElement* vol = gst_bin_get_by_name(GST_BIN(m_gst_pipeline), "audio_volume"); + if (vol) { + g_object_set(vol, "volume", static_cast(volume), NULL); + gst_object_unref(vol); + } +} + +void GstRtpReceiver::set_audio_enabled(bool enabled) { + // Always record intent and rebuild — no early-out on "unchanged", so tapping + // the switch re-evaluates device availability and acts as a retry once a + // hot-plugged sink is back (the switch reports m_audio_active, so it may read + // off while intent is on). + m_audio_enabled = enabled; + spdlog::info("[AUDIO] Runtime toggle -> {}", enabled ? "on" : "off"); + + // Only the live streaming pipeline carries the audio branch; during DVR file + // playback (or before start), just remember the choice for the next stream. + if (m_file_playback || m_gst_pipeline == nullptr) { + return; + } + + // Rebuild off-thread: switch_to_stream() tears down the pipeline and joins + // the pull/socket threads, which must not run on a GStreamer or UI thread. + std::thread([this]() { switch_to_stream(); }).detach(); +} + +void GstRtpReceiver::set_audio_device(const std::string& device) { + if (device == m_audio_device) { + return; + } + m_audio_device = device; + spdlog::info("[AUDIO] Output device -> {}", device.empty() ? "default" : device); + + // Only relevant while the live audio branch exists; otherwise the new device + // is picked up the next time audio is (re)built. + if (m_file_playback || m_gst_pipeline == nullptr || !m_audio_enabled) { + return; + } + std::thread([this]() { switch_to_stream(); }).detach(); +} + void GstRtpReceiver::set_codec_changed_callback(std::function cb) { std::lock_guard lock(m_codec_changed_mutex); m_on_codec_changed = std::move(cb); @@ -1552,6 +1944,458 @@ void GstRtpReceiver::skip_duration(int64_t skip_ms) { } } +// --- DVR record branch (splitmuxsink) --------------------------------------- + +// Drop any ELEMENT messages queued on the pipeline bus. Called before EOS so a +// fragment-closed from an earlier size-split can't be mistaken for this stop's. +static void dvr_drain_element_messages(GstBus* bus) { + if (!bus) return; + GstMessage* m; + while ((m = gst_bus_pop_filtered(bus, GST_MESSAGE_ELEMENT)) != nullptr) gst_message_unref(m); +} + +// Wait (bounded) for the splitmuxsink to post fragment-closed after an EOS, so +// the mp4 moov is written before the bin is torn down. +static void dvr_wait_fragment_closed(GstBus* bus) { + if (!bus) return; + const gint64 deadline = g_get_monotonic_time() + G_TIME_SPAN_SECOND * 2; + bool closed = false; + while (!closed && g_get_monotonic_time() < deadline) { + GstMessage* msg = gst_bus_timed_pop_filtered(bus, 100 * GST_MSECOND, GST_MESSAGE_ELEMENT); + if (!msg) continue; + if (gst_message_has_name(msg, "splitmuxsink-fragment-closed")) closed = true; + gst_message_unref(msg); + } + if (!closed) spdlog::warn("[DVR] timed out waiting for mp4 finalization"); +} + +// Expose an in-bin element's sink pad as a ghost pad so an external tee can link +// into the bin. Returns a reffed pad (or nullptr). +static GstPad* dvr_ghost_sink(GstElement* bin, const char* elem, const char* gname) { + GstElement* e = gst_bin_get_by_name(GST_BIN(bin), elem); + if (!e) return nullptr; + GstPad* p = gst_element_get_static_pad(e, "sink"); + GstPad* g = gst_ghost_pad_new(gname, p); + gst_pad_set_active(g, TRUE); + gst_element_add_pad(bin, g); + gst_object_unref(p); + gst_object_unref(e); + return gst_element_get_static_pad(bin, gname); // reffed +} + +// gst_element_request_pad_simple() only exists since GStreamer 1.19.1; older +// distros (Debian Bullseye ships 1.18) provide the now-deprecated +// gst_element_get_request_pad(). Pick the right one per build so both compile +// cleanly (no missing symbol on 1.18, no deprecation warning on 1.20+). +static GstPad* dvr_request_tee_pad(GstElement* tee) { +#if GST_CHECK_VERSION(1, 19, 1) + return gst_element_request_pad_simple(tee, "src_%u"); +#else + return gst_element_get_request_pad(tee, "src_%u"); +#endif +} + +void GstRtpReceiver::set_dvr_config(int64_t max_size_bytes, std::function base_path_fn) { + std::lock_guard lk(m_dvr_cfg_mutex); + m_dvr_max_size = max_size_bytes > 0 ? max_size_bytes : 0; + m_dvr_base_path_fn = std::move(base_path_fn); +} + +void GstRtpReceiver::dvr_set_max_size(int64_t max_size_bytes) { + std::lock_guard lk(m_dvr_cfg_mutex); + m_dvr_max_size = max_size_bytes > 0 ? max_size_bytes : 0; + // Apply live to an in-progress recording (honoured at the next split). The + // element, if present, is only torn down by the pull thread; this setter is + // called from the menu thread, so read it once under no additional lock — + // a torn-down bin just means the value applies to the next start. + if (m_dvr_rec_bin) { + GstElement* sms = gst_bin_get_by_name(GST_BIN(m_dvr_rec_bin), "dvr_sms"); + if (sms) { + g_object_set(sms, "max-size-bytes", static_cast(m_dvr_max_size), NULL); + gst_object_unref(sms); + } + } +} + +void GstRtpReceiver::dvr_request_recording(bool on) { + m_dvr_want.store(on, std::memory_order_relaxed); +} + +gchar* GstRtpReceiver::dvr_format_location(GstElement* splitmux, guint fragment_id, gpointer) { + const char* base = static_cast(g_object_get_data(G_OBJECT(splitmux), "dvr-base")); + if (!base || !base[0]) base = "/tmp/pixelpilot_record"; + // First fragment keeps the plain name; size-splits get _partN (N from 2), to + // match the previous minimp4 recorder's split naming. + if (fragment_id == 0) return g_strdup_printf("%s.mp4", base); + return g_strdup_printf("%s_part%u.mp4", base, fragment_id + 1); +} + +void GstRtpReceiver::dvr_tick() { + const bool want = m_dvr_want.load(std::memory_order_relaxed) + && m_gst_pipeline != nullptr && !m_file_playback; + const bool active = (m_dvr_rec_bin != nullptr); + if (want && !active) { + dvr_add_record_bin(); + } else if (!want && active) { + dvr_remove_record_bin(); + } + dvr_reenc_tick(); +} + +void GstRtpReceiver::dvr_add_record_bin() { + if (!m_gst_pipeline) return; + GstElement* tee = gst_bin_get_by_name(GST_BIN(m_gst_pipeline), "rtp_tee"); + if (!tee) { spdlog::warn("[DVR] no rtp_tee in pipeline; cannot record"); return; } + + std::string base; + int64_t max_bytes; + { + std::lock_guard lk(m_dvr_cfg_mutex); + if (m_dvr_base_path_fn) base = m_dvr_base_path_fn(); + max_bytes = m_dvr_max_size; + } + if (base.empty()) { + spdlog::error("[DVR] no output path resolved; recording disabled"); + m_dvr_want.store(false, std::memory_order_relaxed); // avoid per-tick retry spam + gst_object_unref(tee); + return; + } + + const bool h265 = (m_video_codec == VideoCodec::H265); + // Only mux an audio track when Opus is actually flowing: recording an empty + // audio track (--audio on but the air sends none) yields an unplayable mp4. + const bool audio = m_audio_active && audio_recently_seen(); + // Native video (no re-encode) + muxed Opus (when active) into one mp4 via + // splitmuxsink. A leading queue on each input decouples the record branch + // from the tee so a slow disk cannot back-pressure and stall the live video. + std::stringstream ss; + ss << "queue name=dvr_vq max-size-buffers=0 max-size-bytes=0 max-size-time=0 ! " + << pipeline::gst_rtp_video_caps_fields(m_video_codec) << " ! " + << (h265 ? "rtph265depay" : "rtph264depay") << " name=dvr_vdepay ! " + << (h265 ? "h265parse" : "h264parse") << " config-interval=-1 ! " + << "splitmuxsink name=dvr_sms muxer=mp4mux max-size-bytes=" << static_cast(max_bytes); + if (audio) { + // capssetter (ANY sink template) hands Opus RTP caps to the depayloader + // without constraining the tee; a PT probe drops non-audio packets. + ss << " queue name=dvr_aq leaky=downstream max-size-buffers=0 max-size-bytes=0 max-size-time=200000000 ! " + << "capssetter replace=true caps=\"application/x-rtp, media=(string)audio, clock-rate=(int)48000," + << " encoding-name=(string)OPUS, payload=(int)" << m_audio_pt << "\" ! " + << "rtpopusdepay name=dvr_adepay ! opusparse ! dvr_sms.audio_0"; + } + + GError* err = nullptr; + GstElement* bin = gst_parse_bin_from_description(ss.str().c_str(), FALSE, &err); + if (!bin) { + spdlog::error("[DVR] failed to build record bin: {}", err ? err->message : "unknown"); + if (err) g_error_free(err); + gst_object_unref(tee); + m_dvr_want.store(false, std::memory_order_relaxed); + return; + } + gst_bin_add(GST_BIN(m_gst_pipeline), bin); + // Contain the record bin's async state change so adding it to the live + // pipeline can't back-pressure/stall it. Without this, starting a second + // recorder (BOTH mode) deadlocks both splitmuxsinks (they sleep forever on an + // undefined running time and the udpsrc stalls). + g_object_set(bin, "async-handling", TRUE, NULL); + + // Custom split filenames from the resolved base path (freed with the sink). + GstElement* sms = gst_bin_get_by_name(GST_BIN(bin), "dvr_sms"); + if (sms) { + g_object_set(sms, "async-handling", TRUE, NULL); + g_object_set_data_full(G_OBJECT(sms), "dvr-base", g_strdup(base.c_str()), g_free); + g_signal_connect(sms, "format-location", G_CALLBACK(&GstRtpReceiver::dvr_format_location), nullptr); + gst_object_unref(sms); + } + + // Payload-type demux for the record branch when audio is muxed into the flow. + if (audio) { + const uint8_t pt = static_cast(m_audio_pt); + attach_pt_filter(m_gst_pipeline, "dvr_vdepay", pt, /*drop_when_match=*/true); + attach_pt_filter(m_gst_pipeline, "dvr_adepay", pt, /*drop_when_match=*/false); + } + + // Expose the input queue sinks as ghost pads so the tee can link into the bin. + GstPad* gv = dvr_ghost_sink(bin, "dvr_vq", "vsink"); + GstPad* ga = audio ? dvr_ghost_sink(bin, "dvr_aq", "asink") : nullptr; + + // Sync to PLAYING BEFORE linking to the live tee: linking a still-NULL branch + // makes the tee push into it and the udpsrc pauses with FLUSHING (kills video). + if (!gst_element_sync_state_with_parent(bin)) { + spdlog::error("[DVR] record bin failed to reach PLAYING; aborting record"); + if (gv) gst_object_unref(gv); + if (ga) gst_object_unref(ga); + gst_element_set_state(bin, GST_STATE_NULL); + gst_bin_remove(GST_BIN(m_gst_pipeline), bin); + gst_object_unref(tee); + m_dvr_want.store(false, std::memory_order_relaxed); + return; + } + + m_dvr_tee_video_pad = dvr_request_tee_pad(tee); + if (gv) gst_pad_link(m_dvr_tee_video_pad, gv); + if (ga) { + m_dvr_tee_audio_pad = dvr_request_tee_pad(tee); + gst_pad_link(m_dvr_tee_audio_pad, ga); + } + if (gv) gst_object_unref(gv); + if (ga) gst_object_unref(ga); + gst_object_unref(tee); + + m_dvr_rec_bin = bin; + m_dvr_active.store(true, std::memory_order_relaxed); + spdlog::info("[DVR] recording -> {}.mp4 (video={}, audio={}, split={}MB)", + base, h265 ? "H.265" : "H.264", audio ? "opus" : "none", + max_bytes / 1000000); + + // Ask the air side for a keyframe so the first fragment opens promptly + // (splitmuxsink starts a file only on a keyframe boundary). + idr_request_record_start(); +} + +void GstRtpReceiver::dvr_remove_record_bin() { + if (!m_dvr_rec_bin) return; + GstElement* bin = m_dvr_rec_bin; + GstElement* tee = m_gst_pipeline ? gst_bin_get_by_name(GST_BIN(m_gst_pipeline), "rtp_tee") : nullptr; + + GstPad* gv = gst_element_get_static_pad(bin, "vsink"); + GstPad* ga = gst_element_get_static_pad(bin, "asink"); + + if (m_dvr_tee_video_pad && gv) gst_pad_unlink(m_dvr_tee_video_pad, gv); + if (m_dvr_tee_audio_pad && ga) gst_pad_unlink(m_dvr_tee_audio_pad, ga); + if (tee && m_dvr_tee_video_pad) gst_element_release_request_pad(tee, m_dvr_tee_video_pad); + if (tee && m_dvr_tee_audio_pad) gst_element_release_request_pad(tee, m_dvr_tee_audio_pad); + + // Finalize: EOS the bin inputs so the splitmuxsink writes the mp4 moov, then + // wait (bounded) for THIS stop's fragment-closed. Stale fragment-closed + // messages from earlier size-splits are drained first so we don't stop early. + GstBus* bus = m_gst_pipeline ? gst_element_get_bus(m_gst_pipeline) : nullptr; + dvr_drain_element_messages(bus); + if (gv) gst_pad_send_event(gv, gst_event_new_eos()); + if (ga) gst_pad_send_event(ga, gst_event_new_eos()); + dvr_wait_fragment_closed(bus); + if (bus) gst_object_unref(bus); + + if (gv) gst_object_unref(gv); + if (ga) gst_object_unref(ga); + + gst_element_set_state(bin, GST_STATE_NULL); + if (m_gst_pipeline) gst_bin_remove(GST_BIN(m_gst_pipeline), bin); + + if (m_dvr_tee_video_pad) { gst_object_unref(m_dvr_tee_video_pad); m_dvr_tee_video_pad = nullptr; } + if (m_dvr_tee_audio_pad) { gst_object_unref(m_dvr_tee_audio_pad); m_dvr_tee_audio_pad = nullptr; } + if (tee) gst_object_unref(tee); + m_dvr_rec_bin = nullptr; + m_dvr_active.store(false, std::memory_order_relaxed); + spdlog::info("[DVR] recording stopped"); +} + +// --- Re-encode record branch (appsrc video + Opus) -------------------------- + +void GstRtpReceiver::dvr_reenc_set_config(VideoCodec codec, int64_t max_size_bytes, + std::function base_path_fn) { + std::lock_guard lk(m_dvr_reenc_cfg_mutex); + m_dvr_reenc_codec = codec; + m_dvr_reenc_max_size = max_size_bytes > 0 ? max_size_bytes : 0; + m_dvr_reenc_base_path_fn = std::move(base_path_fn); + if (m_dvr_reenc_bin) { + GstElement* sms = gst_bin_get_by_name(GST_BIN(m_dvr_reenc_bin), "dvr_reenc_sms"); + if (sms) { + g_object_set(sms, "max-size-bytes", static_cast(m_dvr_reenc_max_size), NULL); + gst_object_unref(sms); + } + } +} + +void GstRtpReceiver::set_dvr_reenc_on_start(std::function cb) { + std::lock_guard lk(m_dvr_reenc_cfg_mutex); + m_dvr_reenc_on_start = std::move(cb); +} + +void GstRtpReceiver::dvr_reenc_request_recording(bool on) { + m_dvr_reenc_want.store(on, std::memory_order_relaxed); +} + +void GstRtpReceiver::dvr_reenc_roll() { + m_dvr_reenc_roll_pending.store(true, std::memory_order_relaxed); +} + +void GstRtpReceiver::dvr_reenc_push(std::shared_ptr> nal) { + if (!nal || nal->empty()) return; + std::lock_guard lk(m_dvr_reenc_src_mutex); + if (!m_dvr_reenc_appsrc) return; // not recording (yet); drop + GstBuffer* buf = gst_buffer_new_allocate(nullptr, nal->size(), nullptr); + gst_buffer_fill(buf, 0, nal->data(), nal->size()); + if (gst_app_src_push_buffer(GST_APP_SRC(m_dvr_reenc_appsrc), buf) != GST_FLOW_OK) { + // The branch is being torn down or blocked; drop quietly. + } +} + +void GstRtpReceiver::dvr_reenc_tick() { + const bool want = m_dvr_reenc_want.load(std::memory_order_relaxed) + && m_gst_pipeline != nullptr && !m_file_playback; + const bool active = (m_dvr_reenc_bin != nullptr); + // A pending roll (codec/resolution/fps change) tears the current file down; + // the next tick re-opens with the new config if recording is still wanted. + if (active && m_dvr_reenc_roll_pending.exchange(false, std::memory_order_relaxed)) { + dvr_remove_reenc_bin(); + return; + } + if (want && !active) { + dvr_add_reenc_bin(); + } else if (!want && active) { + dvr_remove_reenc_bin(); + } +} + +void GstRtpReceiver::dvr_add_reenc_bin() { + if (!m_gst_pipeline) return; + GstElement* tee = gst_bin_get_by_name(GST_BIN(m_gst_pipeline), "rtp_tee"); + if (!tee) { spdlog::warn("[DVR/reenc] no rtp_tee; cannot record"); return; } + + std::string base; + int64_t max_bytes; + VideoCodec codec; + std::function on_start; + { + std::lock_guard lk(m_dvr_reenc_cfg_mutex); + if (m_dvr_reenc_base_path_fn) base = m_dvr_reenc_base_path_fn(); + max_bytes = m_dvr_reenc_max_size; + codec = m_dvr_reenc_codec; + on_start = m_dvr_reenc_on_start; + } + if (base.empty()) { + spdlog::error("[DVR/reenc] no output path resolved; recording disabled"); + m_dvr_reenc_want.store(false, std::memory_order_relaxed); + gst_object_unref(tee); + return; + } + + const bool h265 = (codec == VideoCodec::H265); + // Only mux audio when Opus is actually flowing — an empty audio track (--audio + // on but the air sends none) makes the mp4 unplayable. + const bool audio = m_audio_active && audio_recently_seen(); + // Video is pushed in from the encoder thread (do-timestamp => live mux). Audio + // taps the same tee/Opus as the raw recorder. + std::stringstream ss; + ss << "appsrc name=dvr_reenc_src is-live=true do-timestamp=true format=time ! " + << (h265 ? "h265parse" : "h264parse") << " config-interval=-1 ! " + << "splitmuxsink name=dvr_reenc_sms muxer=mp4mux max-size-bytes=" << static_cast(max_bytes); + if (audio) { + ss << " queue name=dvr_reenc_aq leaky=downstream max-size-buffers=0 max-size-bytes=0 max-size-time=200000000 ! " + << "capssetter replace=true caps=\"application/x-rtp, media=(string)audio, clock-rate=(int)48000," + << " encoding-name=(string)OPUS, payload=(int)" << m_audio_pt << "\" ! " + << "rtpopusdepay name=dvr_reenc_adepay ! opusparse ! dvr_reenc_sms.audio_0"; + } + + GError* err = nullptr; + GstElement* bin = gst_parse_bin_from_description(ss.str().c_str(), FALSE, &err); + if (!bin) { + spdlog::error("[DVR/reenc] failed to build record bin: {}", err ? err->message : "unknown"); + if (err) g_error_free(err); + gst_object_unref(tee); + m_dvr_reenc_want.store(false, std::memory_order_relaxed); + return; + } + gst_bin_add(GST_BIN(m_gst_pipeline), bin); + // Contain the record bin's async state change (see dvr_add_record_bin) so + // coexisting with the raw recorder in BOTH mode doesn't deadlock. + g_object_set(bin, "async-handling", TRUE, NULL); + + // appsrc caps + config (byte-stream elementary video from the MPP encoder). + GstElement* src = gst_bin_get_by_name(GST_BIN(bin), "dvr_reenc_src"); + GstCaps* caps = gst_caps_new_simple(h265 ? "video/x-h265" : "video/x-h264", + "stream-format", G_TYPE_STRING, "byte-stream", + "alignment", G_TYPE_STRING, "au", NULL); + g_object_set(src, "caps", caps, "max-bytes", (guint64)(8 * 1024 * 1024), "block", FALSE, NULL); + gst_caps_unref(caps); + + // Custom split filenames from the resolved base path (freed with the sink). + GstElement* sms = gst_bin_get_by_name(GST_BIN(bin), "dvr_reenc_sms"); + if (sms) { + g_object_set(sms, "async-handling", TRUE, NULL); + g_object_set_data_full(G_OBJECT(sms), "dvr-base", g_strdup(base.c_str()), g_free); + g_signal_connect(sms, "format-location", G_CALLBACK(&GstRtpReceiver::dvr_format_location), nullptr); + gst_object_unref(sms); + } + + // Audio branch: keep non-audio-PT packets off the Opus depayloader. + GstPad* ga = nullptr; + if (audio) { + attach_pt_filter(m_gst_pipeline, "dvr_reenc_adepay", static_cast(m_audio_pt), /*drop_when_match=*/false); + ga = dvr_ghost_sink(bin, "dvr_reenc_aq", "asink"); + } + + // Sync to PLAYING before linking the audio tee pad (same FLUSHING guard as raw). + if (!gst_element_sync_state_with_parent(bin)) { + spdlog::error("[DVR/reenc] record bin failed to reach PLAYING; aborting record"); + if (ga) gst_object_unref(ga); + if (src) gst_object_unref(src); + gst_element_set_state(bin, GST_STATE_NULL); + gst_bin_remove(GST_BIN(m_gst_pipeline), bin); + gst_object_unref(tee); + m_dvr_reenc_want.store(false, std::memory_order_relaxed); + return; + } + if (ga) { + m_dvr_reenc_tee_audio_pad = dvr_request_tee_pad(tee); + gst_pad_link(m_dvr_reenc_tee_audio_pad, ga); + gst_object_unref(ga); + } + gst_object_unref(tee); + + // Publish the appsrc for the encoder thread's dvr_reenc_push() (transfers ref). + { + std::lock_guard lk(m_dvr_reenc_src_mutex); + m_dvr_reenc_appsrc = src; + } + m_dvr_reenc_bin = bin; + m_dvr_reenc_active.store(true, std::memory_order_relaxed); + spdlog::info("[DVR/reenc] recording -> {}.mp4 (video={}, audio={}, split={}MB)", + base, h265 ? "H.265" : "H.264", audio ? "opus" : "none", max_bytes / 1000000); + + idr_request_record_start(); // air-side keyframe + if (on_start) on_start(); // encoder-side keyframe (open first fragment) +} + +void GstRtpReceiver::dvr_remove_reenc_bin() { + if (!m_dvr_reenc_bin) return; + GstElement* bin = m_dvr_reenc_bin; + GstElement* tee = m_gst_pipeline ? gst_bin_get_by_name(GST_BIN(m_gst_pipeline), "rtp_tee") : nullptr; + + // Stop the encoder feed first: null the appsrc so dvr_reenc_push() drops. + GstElement* src = nullptr; + { + std::lock_guard lk(m_dvr_reenc_src_mutex); + src = m_dvr_reenc_appsrc; + m_dvr_reenc_appsrc = nullptr; + } + + GstPad* ga = gst_element_get_static_pad(bin, "asink"); + if (m_dvr_reenc_tee_audio_pad && ga) gst_pad_unlink(m_dvr_reenc_tee_audio_pad, ga); + if (tee && m_dvr_reenc_tee_audio_pad) gst_element_release_request_pad(tee, m_dvr_reenc_tee_audio_pad); + + // Finalize: EOS the appsrc (video) + audio ghost, wait for fragment-closed. + GstBus* bus = m_gst_pipeline ? gst_element_get_bus(m_gst_pipeline) : nullptr; + dvr_drain_element_messages(bus); + if (src) gst_app_src_end_of_stream(GST_APP_SRC(src)); + if (ga) gst_pad_send_event(ga, gst_event_new_eos()); + dvr_wait_fragment_closed(bus); + if (bus) gst_object_unref(bus); + + if (ga) gst_object_unref(ga); + if (src) gst_object_unref(src); // release the ref held in m_dvr_reenc_appsrc + + gst_element_set_state(bin, GST_STATE_NULL); + if (m_gst_pipeline) gst_bin_remove(GST_BIN(m_gst_pipeline), bin); + + if (m_dvr_reenc_tee_audio_pad) { gst_object_unref(m_dvr_reenc_tee_audio_pad); m_dvr_reenc_tee_audio_pad = nullptr; } + if (tee) gst_object_unref(tee); + m_dvr_reenc_bin = nullptr; + m_dvr_reenc_active.store(false, std::memory_order_relaxed); + spdlog::info("[DVR/reenc] recording stopped"); +} + void idr_set_enabled(bool enabled) { g_idr_enabled.store(enabled, std::memory_order_relaxed); } diff --git a/src/gstrtpreceiver.h b/src/gstrtpreceiver.h index e3f1c1ef..a12dd285 100644 --- a/src/gstrtpreceiver.h +++ b/src/gstrtpreceiver.h @@ -17,6 +17,8 @@ #include #include #include +#include +#include #define MAX_PACKET_SIZE 4096 #define RTP_HEADER_LEN 12 @@ -49,6 +51,27 @@ class GstRtpReceiver { explicit GstRtpReceiver(int udp_port, const VideoCodec& codec); explicit GstRtpReceiver(const char *s, const VideoCodec& codec); virtual ~GstRtpReceiver(); + // Enable receiving Opus audio muxed into the same RTP flow as the video + // (distinguished by payload type). Must be called before start_receiving(). + // device is an ALSA device string ("" = system default); pt is the audio + // RTP payload type (OpenIPC/majestic default 98). + void configure_audio(bool enabled, const std::string& device, int pt, double volume = 1.0); + // Toggle the Opus audio branch at runtime (e.g. from the OSD menu). Rebuilds + // the live streaming pipeline; a no-op if the state is unchanged or while a + // DVR file is playing (the choice then applies on the next switch_to_stream). + void set_audio_enabled(bool enabled); + // Reports the *effective* state (is the Opus branch actually up), not just the + // intent — so the OSD switch reads off when the selected sink is unavailable + // and audio has fallen back to video-only. + bool get_audio_enabled() const { return m_audio_active; } + // Select the ALSA output for audio: an /proc/asound/cards id (e.g. + // "rockchiphdmi"), a full ALSA device string, or "" / "default" for the + // system default. Rebuilds the live pipeline if audio is currently playing. + void set_audio_device(const std::string& device); + // Software output volume, 0.0..1.0 (1.0 = unity/100%). Applied live to the + // pipeline's volume element (no rebuild); persists for the next build. + void set_audio_volume(double volume); + double get_audio_volume() const { return m_audio_volume; } // Depending on the codec, these are h264,h265 or mjpeg "frames" / frame buffers // The big advantage of gstreamer is that it seems to handle all those parsing quirks the best, // e.g. the frames on this cb should be easily passable to whatever decode api is available. @@ -71,6 +94,44 @@ class GstRtpReceiver { // Invoked (on an internal thread) after a mid-stream codec switch has been // detected and the pipeline rebuilt, so the host can realign its decoder. void set_codec_changed_callback(std::function cb); + + // --- DVR recording (in-pipeline; replaces the raw minimp4 recorder) ------- + // Records the live stream to mp4 via a splitmuxsink branch teed off the RTP + // flow: native video (no re-encode) plus the muxed Opus audio when active, + // so both tracks share one timebase and stay in sync (fixing the drift of + // the old fixed-framerate raw muxer). Splitting by size and file naming are + // handled here; the caller supplies the naming policy and size limit. + // + // base_path_fn returns the next recording's path WITHOUT extension (the + // first file is .mp4, size-splits are _partN.mp4). It runs on + // the pull thread when recording starts, so it may scan the output dir. + void set_dvr_config(int64_t max_size_bytes, std::function base_path_fn); + void dvr_set_max_size(int64_t max_size_bytes); + // Request recording on/off. Safe from any thread including a signal handler: + // it only stores intent (an atomic); the pull thread performs the pipeline + // surgery on its next tick. Idempotent. + void dvr_request_recording(bool on); + bool dvr_is_recording() const { return m_dvr_active.load(std::memory_order_relaxed); } + + // --- Re-encode DVR (second recorder) -------------------------------------- + // Muxes the MPP-re-encoded video (pushed via dvr_reenc_push, e.g. from the + // encoder's output callback) with the SAME Opus audio into a second mp4 via + // its own appsrc->splitmuxsink branch. Unlike the raw recorder the video is + // on a wall-clock/re-paced timeline (not the RTP clock), so this is a live + // mux (appsrc do-timestamp) — sync is close but a small constant offset may + // remain. codec is the re-encoder's output codec (sets the appsrc caps). + void dvr_reenc_set_config(VideoCodec codec, int64_t max_size_bytes, std::function base_path_fn); + void dvr_reenc_request_recording(bool on); + void dvr_reenc_push(std::shared_ptr> nal); + bool dvr_reenc_is_recording() const { return m_dvr_reenc_active.load(std::memory_order_relaxed); } + // Roll the re-encode file if one is open: call after an encoder codec/ + // resolution/fps change, whose new SPS/dimensions cannot be applied to an + // already-open mp4 track. A no-op when not recording. + void dvr_reenc_roll(); + // Invoked (on the pull thread) right after the re-encode branch goes live, so + // the host can force the encoder to emit a keyframe (splitmuxsink opens the + // first fragment only on a keyframe). + void set_dvr_reenc_on_start(std::function cb); private: // Rebuild the pipeline for new_codec after a mid-stream switch is detected. void request_codec_switch(VideoCodec new_codec); @@ -78,6 +139,11 @@ class GstRtpReceiver { std::string construct_file_playback_pipeline(const char * file_path); void loop_pull_samples(); void on_new_sample(std::shared_ptr> sample); + // Drain the pipeline bus (on the pull thread) so error/warning spam from a + // failing sink can't accumulate unbounded, and fall back to video-only if the + // audio sink dies — e.g. a USB headset unplugged mid-flight, which otherwise + // spins alsasink at 100% CPU and grows memory until it's exhausted. + void handle_bus_messages(); // The gstreamer pipeline GstElement * m_gst_pipeline=nullptr; NEW_FRAME_CALLBACK m_cb; @@ -86,6 +152,24 @@ class GstRtpReceiver { // builds for H.265 and mid-stream codec-switch detection is enabled. False // when the user pinned a codec, in which case we never override their choice. bool m_auto_codec = false; + // Audio (Opus) config. m_audio_enabled / m_audio_device are the user's intent + // (what the CLI/menu asked for, and what get_audio_enabled() reports). + // m_audio_active is the effective state after switch_to_stream() checks the + // Opus/ALSA stack and the selected output device are usable; when the sink is + // missing it drops to false (video-only) while the intent/selection is kept. + bool m_audio_enabled = false; + bool m_audio_active = false; + std::string m_audio_device; + double m_audio_volume = 1.0; + int m_audio_pt = 98; + // Set while a video-only fallback rebuild (after an audio-sink failure) is in + // flight, so the pull thread spawns exactly one rebuild. + std::atomic m_audio_rebuilding{false}; + // True while a switch_to_file_playback() pipeline is up (no live audio branch). + bool m_file_playback = false; + // Serializes switch_to_stream() so a menu audio-toggle and an automatic + // codec-switch rebuild can never tear down/rebuild the pipeline at once. + std::mutex m_stream_mutex; VideoCodec m_playback_codec = VideoCodec::UNKNOWN; // Notified after a detected mid-stream codec switch + pipeline rebuild. std::function m_on_codec_changed; @@ -106,6 +190,42 @@ class GstRtpReceiver { double m_playback_rate = 1.0; bool m_is_paused = false; double m_pre_pause_rate = 1.0; + + // DVR record branch. All pipeline surgery runs on the pull thread (dvr_tick, + // called each pull iteration), whose lifetime is bounded by the pipeline's + // (started after PLAYING, joined before teardown), so it needs no extra lock + // against switch_to_stream()/stop_receiving(). + void dvr_tick(); + void dvr_add_record_bin(); + void dvr_remove_record_bin(); + static gchar* dvr_format_location(GstElement* splitmux, guint fragment_id, gpointer user_data); + std::atomic m_dvr_want{false}; // caller's intent (recording on/off) + std::atomic m_dvr_active{false}; // record bin currently present + int64_t m_dvr_max_size = 0; // splitmuxsink max-size-bytes (0 = no split) + std::function m_dvr_base_path_fn; + std::mutex m_dvr_cfg_mutex; // guards m_dvr_base_path_fn / m_dvr_max_size + GstElement* m_dvr_rec_bin = nullptr; + GstPad* m_dvr_tee_video_pad = nullptr; + GstPad* m_dvr_tee_audio_pad = nullptr; + + // Re-encode recorder (appsrc video + Opus). Same pull-thread-driven lifecycle + // as the raw recorder; the appsrc is pushed to from the encoder thread, hence + // its own guarding mutex. + void dvr_reenc_tick(); + void dvr_add_reenc_bin(); + void dvr_remove_reenc_bin(); + std::atomic m_dvr_reenc_want{false}; + std::atomic m_dvr_reenc_active{false}; + std::atomic m_dvr_reenc_roll_pending{false}; + int64_t m_dvr_reenc_max_size = 0; + VideoCodec m_dvr_reenc_codec = VideoCodec::H264; + std::function m_dvr_reenc_base_path_fn; + std::function m_dvr_reenc_on_start; + std::mutex m_dvr_reenc_cfg_mutex; // guards config + on_start + GstElement* m_dvr_reenc_bin = nullptr; + GstElement* m_dvr_reenc_appsrc = nullptr; + std::mutex m_dvr_reenc_src_mutex; // guards m_dvr_reenc_appsrc (pushed from encoder thread) + GstPad* m_dvr_reenc_tee_audio_pad = nullptr; }; #endif diff --git a/src/input.cpp b/src/input.cpp index 1b89c21a..c24a8fc1 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -14,6 +14,7 @@ #include "lvgl/lvgl.h" #include "input.h" #include "menu.h" +#include "osd.h" /* osd_publish_int_fact() for the +/- test-fact keys */ extern YAML::Node config; extern lv_group_t *main_group; @@ -433,6 +434,20 @@ void toggle_screen(void) { } } +// TEMP debug: the +/- keys nudge a test fact (test.value, clamped -100..100) so +// OSD widgets can be exercised without a live link. Bind a widget to +// { "name": "test.value" } to drive it. +static void test_fact_bump(int delta) { + static int v = 0; + v += delta; + if (v > 100) v = 100; + else if (v < -100) v = -100; +#ifndef USE_SIMULATOR + osd_publish_int_fact("test.value", NULL, 0, v); // osd.cpp isn't in the sim build +#endif + printf("test.value = %d\n", v); +} + // Handle WASD input and convert to LVGL key codes void handle_keyboard_input(void) { char c; @@ -547,6 +562,13 @@ void handle_keyboard_input(void) { toggle_rec_enabled(); /* simulate the hardware record button (sim only) */ break; #endif + case '+': + case '=': // unshifted '+' + test_fact_bump(+5); + break; + case '-': + test_fact_bump(-5); + break; case 'q': case 'Q': raise(SIGINT); diff --git a/src/main.cpp b/src/main.cpp index 410ea20c..f098da45 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -113,9 +113,15 @@ uint32_t refresh_frequency_ms = 1000; VideoCodec codec = VideoCodec::H265; uint16_t listen_port = 5600; const char* unix_socket = NULL; +// Opt-in Opus audio (muxed into the RTP stream, split by payload type). +bool audio_enabled = false; +std::string audio_device; // empty = ALSA system default +int audio_pt = 98; // OpenIPC/majestic default RTP payload type for audio +int audio_volume = 100; // software output volume, percent (0..100) char* dvr_template = NULL; -Dvr *dvr_raw = NULL; -Dvr *dvr_reenc_inst = NULL; +// Both DVR paths are now recorded in-pipeline by the receiver (raw off the RTP +// tee; re-encode via an appsrc fed by the MPP encoder below), each muxed with +// Opus via splitmuxsink. The minimp4 Dvr is retired. MppEncoder *reencoder = NULL; MppEncoderParams reenc_params; DvrMode dvr_mode = DVR_MODE_RAW; @@ -125,11 +131,13 @@ static bool dvr_filenames_with_sequence = false; static int mp4_fragmentation_mode = 0; static int64_t dvr_max_file_size = 4000000000LL; // 4 GB (decimal), safe margin for VFAT 4 GiB limit FrameProcessor *frame_proc = nullptr; +// Receiver owns the live pipeline AND the in-pipeline raw DVR recorder; declared +// here (not just before the extern "C" block) so the SIGUSR1 record-button +// handler can flip recording on it. +std::unique_ptr receiver; // Thread handles for the encoder and pacer — file-scope so live mode toggle can join them. static pthread_t g_tid_enc = 0; static pthread_t g_tid_fproc = 0; -static pthread_t g_tid_dvr_raw = 0; -static pthread_t g_tid_dvr_reenc = 0; // Decoded frame geometry – updated in init_buffer(), used in __FRAME_THREAD__ uint32_t decoded_hor_stride = 0; @@ -152,12 +160,6 @@ float live_colortrans_offset = -0.15f; float live_colortrans_gain = 2.5f; gamma_lut_controller lut_ctrl; -// Helper: get target width/height for the current re-encode resolution setting. -static void reenc_target_dims(uint32_t &w, uint32_t &h) { - if (reenc_params.resolution == EncResolution::Res720p) { w = 1280; h = 720; } - else { w = 1920; h = 1080; } -} - void init_buffer(MppFrame frame) { output_list->video_frm_width = mpp_frame_get_width(frame); output_list->video_frm_height = mpp_frame_get_height(frame); @@ -276,14 +278,8 @@ void init_buffer(MppFrame frame) { ret = modeset_perform_modeset(drm_fd, output_list, output_list->video_request, &output_list->video_plane, mpi.frame_to_drm[0].fb_id, output_list->video_frm_width, output_list->video_frm_height, video_zpos); assert(ret >= 0); - // dvr setup - if (dvr_raw != NULL) { - dvr_raw->set_video_params(output_list->video_frm_width, output_list->video_frm_height, codec); - } - if (dvr_reenc_inst != NULL) { - uint32_t rw, rh; reenc_target_dims(rw, rh); - dvr_reenc_inst->set_video_params(rw, rh, reenc_params.codec); - } + // Both recorders take their dimensions/codec straight from the parsed + // elementary stream (h26xparse -> mp4mux), so no explicit video params here. } // __FRAME_THREAD__ @@ -510,12 +506,8 @@ void sig_handler(int signum) mavlink_thread_signal++; wfb_thread_signal++; osd_thread_signal++; - if (dvr_raw != NULL) { - dvr_raw->shutdown(); - } - if (dvr_reenc_inst != NULL) { - dvr_reenc_inst->shutdown(); - } + // Both recordings are finalized when the receiver tears the pipeline down + // (stop_receiving stops each recorder, writing its mp4 moov). if (frame_proc != NULL) { frame_proc->shutdown(); } @@ -525,14 +517,75 @@ void sig_handler(int signum) return_value = signum; } +// Base path (no extension) for the in-pipeline raw DVR recording. In BOTH mode +// the raw file carries a _raw suffix (the re-encode file gets _reenc); in RAW +// mode it uses the plain template. Evaluated at record-start time so a runtime +// mode change is reflected. +static std::string dvr_raw_next_base_path() { + if (!dvr_template) return ""; + std::string tpl = dvr_template; + if (dvr_mode == DVR_MODE_BOTH) { + auto dot = tpl.rfind('.'); + if (dot != std::string::npos) tpl.insert(dot, "_raw"); + else tpl += "_raw"; + } + return dvr_next_base_path(tpl.c_str(), dvr_filenames_with_sequence); +} + +// Hand the receiver its DVR naming policy + split size (called once the receiver +// exists). Recording itself is toggled via receiver->dvr_request_recording(). +static void dvr_configure_receiver() { + if (receiver) receiver->set_dvr_config(dvr_max_file_size, dvr_raw_next_base_path); +} + +// The receiver's in-pipeline recorder is the RAW path only; it must stay off in +// reencode-only mode (otherwise it would record a raw file too — and in non-BOTH +// mode collide on the un-suffixed template with the re-encode recorder). +static inline bool dvr_mode_has_raw() { + return dvr_mode == DVR_MODE_RAW || dvr_mode == DVR_MODE_BOTH; +} +static inline bool dvr_mode_has_reenc() { + return dvr_mode == DVR_MODE_REENCODE || dvr_mode == DVR_MODE_BOTH; +} + +// Base path (no extension) for the re-encode recording; _reenc suffix in BOTH. +static std::string dvr_reenc_next_base_path() { + if (!dvr_template) return ""; + std::string tpl = dvr_template; + if (dvr_mode == DVR_MODE_BOTH) { + auto dot = tpl.rfind('.'); + if (dot != std::string::npos) tpl.insert(dot, "_reenc"); + else tpl += "_reenc"; + } + return dvr_next_base_path(tpl.c_str(), dvr_filenames_with_sequence); +} + +// Run by the receiver whenever the reenc recorder starts a file: force an IDR so +// the new mp4 opens on a keyframe, and re-arm the fail-fast latch so a fatal +// conversion error from an earlier recording doesn't kill the fresh one. +static void dvr_reenc_on_start() { + if (reencoder) reencoder->request_idr(); + if (frame_proc) frame_proc->clear_fatal_error(); +} + +// Hand the receiver its re-encode naming policy, split size and output codec. +// Called whenever any of those change (codec/resolution/mode/max-size). +static void dvr_configure_reenc_receiver() { + if (receiver) + receiver->dvr_reenc_set_config(reenc_params.codec, dvr_max_file_size, dvr_reenc_next_base_path); +} + void sigusr1_handler(int signum) { spdlog::info("Received signal {}", signum); bool was_enabled = dvr_enabled; if (was_enabled) { - // Stopping + // Stopping. Only flip atomics here — both recorders' GStreamer surgery is + // done off the receiver's pull thread (request_recording stores intent). spdlog::info("DVR: stopping recording (SIGUSR1)"); - if (dvr_raw) dvr_raw->stop_recording(); - if (dvr_reenc_inst) dvr_reenc_inst->stop_recording(); + if (receiver) { + receiver->dvr_request_recording(false); + receiver->dvr_reenc_request_recording(false); + } dvr_enabled = 0; osd_publish_bool_fact("dvr.recording", NULL, 0, false); } else { @@ -541,8 +594,10 @@ void sigusr1_handler(int signum) { dvr_mode == DVR_MODE_RAW ? "raw" : dvr_mode == DVR_MODE_REENCODE ? "reencode" : "both"); dvr_enabled = 1; osd_publish_bool_fact("dvr.recording", NULL, 0, true); - if (dvr_raw) dvr_raw->start_recording(); - if (dvr_reenc_inst) dvr_reenc_inst->start_recording(); + if (receiver) { + receiver->dvr_request_recording(dvr_mode_has_raw()); + receiver->dvr_reenc_request_recording(dvr_mode_has_reenc()); + } if (reencoder) reencoder->request_idr(); } } @@ -566,34 +621,19 @@ void sigusr2_handler(int signum) { spdlog::info("disable_vsync: {}", disable_vsync); } -// Helper: create a filename template with a suffix inserted before the extension. -// Returns a strdup'd string — caller must free. -static char* dvr_template_with_suffix(const char *tpl, const char *suffix) { - std::string s(tpl); - auto dot = s.rfind('.'); - if (dot != std::string::npos) - s.insert(dot, suffix); - else - s.append(suffix); - return strdup(s.c_str()); -} - -// Shutdown helper for DVR + encoder teardown context. +// Shutdown helper for the re-encode pipeline (frame pacer + encoder) teardown. struct DvrShutdownCtx { - Dvr *dvr_inst; FrameProcessor *p; MppEncoder *e; - pthread_t td, tp, te; + pthread_t tp, te; }; static void *dvr_shutdown_worker(void *arg) { auto *ctx = static_cast(arg); if (ctx->tp) pthread_join(ctx->tp, nullptr); if (ctx->te) pthread_join(ctx->te, nullptr); - if (ctx->td) pthread_join(ctx->td, nullptr); delete ctx->p; delete ctx->e; - delete ctx->dvr_inst; delete ctx; return nullptr; } @@ -601,11 +641,11 @@ static void *dvr_shutdown_worker(void *arg) { // C-compatible interface for gsmenu live control of the DVR. extern "C" { void dvr_reenc_set_fps(int fps) { - if (dvr_reenc_inst) dvr_reenc_inst->stop_recording(); reenc_params.fps = fps; - if (dvr_reenc_inst) dvr_reenc_inst->set_video_framerate(fps); if (frame_proc) frame_proc->set_fps(fps); if (reencoder) reencoder->set_fps(fps); + // Encoder reinit -> roll the re-encode file (new stream discontinuity). + if (receiver) receiver->dvr_reenc_roll(); } void dvr_reenc_set_osd(int enabled) { dvr_osd = (bool)enabled; @@ -632,8 +672,10 @@ extern "C" { void dvr_set_max_size(int mb) { dvr_max_file_size = (int64_t)mb * 1000000LL; - if (dvr_raw) dvr_raw->set_max_file_size(dvr_max_file_size); - if (dvr_reenc_inst) dvr_reenc_inst->set_max_file_size(dvr_max_file_size); + if (receiver) { + receiver->dvr_set_max_size(dvr_max_file_size); + dvr_configure_reenc_receiver(); // pushes the new split size to the reenc branch + } spdlog::info("DVR max file size set to {} MB", mb); } int dvr_get_max_size(void) { return (int)(dvr_max_file_size / 1000000LL); } @@ -646,13 +688,10 @@ extern "C" { } void dvr_reenc_set_resolution(int idx) { - if (dvr_reenc_inst) dvr_reenc_inst->stop_recording(); reenc_params.resolution = (EncResolution)idx; if (frame_proc) frame_proc->set_resolution(reenc_params.resolution); - if (dvr_reenc_inst) { - uint32_t rw, rh; reenc_target_dims(rw, rh); - dvr_reenc_inst->set_video_params(rw, rh, reenc_params.codec); - } + // New frame dimensions can't be applied to an open mp4 track -> roll. + if (receiver) receiver->dvr_reenc_roll(); } void dvr_reenc_set_bitrate(int kbps) { @@ -661,14 +700,12 @@ extern "C" { } void dvr_reenc_set_codec(int idx) { - if (dvr_reenc_inst) dvr_reenc_inst->stop_recording(); VideoCodec vc = (idx == 1) ? VideoCodec::H265 : VideoCodec::H264; reenc_params.codec = vc; if (reencoder) reencoder->set_codec(vc); - if (dvr_reenc_inst) { - uint32_t rw, rh; reenc_target_dims(rw, rh); - dvr_reenc_inst->set_video_params(rw, rh, vc); - } + // New codec changes the appsrc caps of the reenc branch -> reconfigure + roll. + dvr_configure_reenc_receiver(); + if (receiver) receiver->dvr_reenc_roll(); } void dvr_start_all(void) { @@ -676,15 +713,19 @@ extern "C" { dvr_mode == DVR_MODE_RAW ? "raw" : dvr_mode == DVR_MODE_REENCODE ? "reencode" : "both"); dvr_enabled = 1; osd_publish_bool_fact("dvr.recording", NULL, 0, true); - if (dvr_raw) dvr_raw->start_recording(); - if (dvr_reenc_inst) dvr_reenc_inst->start_recording(); + if (receiver) { + receiver->dvr_request_recording(dvr_mode_has_raw()); + receiver->dvr_reenc_request_recording(dvr_mode_has_reenc()); + } if (reencoder) reencoder->request_idr(); } void dvr_stop_all(void) { spdlog::info("DVR: stopping recording"); - if (dvr_raw) dvr_raw->stop_recording(); - if (dvr_reenc_inst) dvr_reenc_inst->stop_recording(); + if (receiver) { + receiver->dvr_request_recording(false); + receiver->dvr_reenc_request_recording(false); + } dvr_enabled = 0; osd_publish_bool_fact("dvr.recording", NULL, 0, false); } @@ -696,7 +737,7 @@ extern "C" { // via an OSD popup so they don't think they got a usable recording. void dvr_reenc_on_fatal_error(void) { spdlog::error("DVR reencode: unrecoverable frame conversion failure, stopping recording"); - if (dvr_reenc_inst) dvr_reenc_inst->stop_recording(); + if (receiver) receiver->dvr_reenc_request_recording(false); osd_publish_str_fact("osd.custom_message", NULL, 0, "DVR reencode failed\nrecording stopped"); // In DVR_MODE_BOTH, raw keeps recording independently and the @@ -711,13 +752,6 @@ extern "C" { } } - /* C-callable wrapper so the menu (C) can set the raw DVR framerate without - * touching the C++ Dvr* directly. */ - void dvr_set_video_framerate(Dvr* dvr, int f); /* defined in dvr.cpp */ - void dvr_set_raw_fps(int fps) { - if (dvr_raw) dvr_set_video_framerate(dvr_raw, fps); - } - // Switch DVR mode at runtime. Stops any active recording. // mode: 0=raw, 1=reencode, 2=both void dvr_set_mode(int mode) { @@ -734,77 +768,35 @@ extern "C" { // Tear down encoder pipeline if no longer needed if (old_has_reenc && !new_has_reenc) { + if (receiver) receiver->dvr_reenc_request_recording(false); FrameProcessor *p = frame_proc; MppEncoder *e = reencoder; - Dvr *d = dvr_reenc_inst; pthread_t tp = g_tid_fproc; pthread_t te = g_tid_enc; - pthread_t td = g_tid_dvr_reenc; frame_proc = nullptr; reencoder = nullptr; - dvr_reenc_inst = nullptr; g_tid_fproc = 0; g_tid_enc = 0; - g_tid_dvr_reenc = 0; if (p) p->shutdown(); if (e) e->shutdown(); - if (d) d->shutdown(); - auto *ctx = new DvrShutdownCtx{d, p, e, td, tp, te}; + auto *ctx = new DvrShutdownCtx{p, e, tp, te}; pthread_t cleanup_tid; pthread_create(&cleanup_tid, NULL, dvr_shutdown_worker, ctx); pthread_detach(cleanup_tid); } - // Tear down raw DVR if no longer needed - if (old_has_raw && !new_has_raw) { - Dvr *d = dvr_raw; - pthread_t td = g_tid_dvr_raw; - dvr_raw = nullptr; - g_tid_dvr_raw = 0; - if (d) d->shutdown(); - auto *ctx = new DvrShutdownCtx{d, nullptr, nullptr, td, 0, 0}; - pthread_t cleanup_tid; - pthread_create(&cleanup_tid, NULL, dvr_shutdown_worker, ctx); - pthread_detach(cleanup_tid); - } - - // Create raw DVR if newly needed - if (new_has_raw && !dvr_raw && dvr_template) { - dvr_thread_params args; - bool both = (new_mode == DVR_MODE_BOTH); - char *tpl = both ? dvr_template_with_suffix(dvr_template, "_raw") : dvr_template; - args.filename_template = tpl; - args.mp4_fragmentation_mode = mp4_fragmentation_mode; - args.dvr_filenames_with_sequence = dvr_filenames_with_sequence; - args.video_framerate = video_framerate; - args.max_file_size = dvr_max_file_size; - args.video_p.video_frm_width = output_list ? output_list->video_frm_width : 0; - args.video_p.video_frm_height = output_list ? output_list->video_frm_height : 0; - args.video_p.codec = codec; - dvr_raw = new Dvr(args); - pthread_create(&g_tid_dvr_raw, NULL, &Dvr::__THREAD__, dvr_raw); - } + // The raw recorder has no per-mode object to create/tear down: it lives + // in the receiver's pipeline and only records while dvr_enabled (already + // cleared by dvr_stop_all above). Its file naming picks up the new mode + // (RAW vs BOTH's _raw suffix) from dvr_raw_next_base_path() at next start. + (void)old_has_raw; (void)new_has_raw; - // Create encoder pipeline + reenc DVR if newly needed + // Create encoder pipeline if newly needed. The re-encoded NALs are muxed + // (with Opus) by the receiver's reenc recorder, not a minimp4 Dvr. if (new_has_reenc && !reencoder && dvr_template) { - bool both = (new_mode == DVR_MODE_BOTH); - char *tpl = both ? dvr_template_with_suffix(dvr_template, "_reenc") : dvr_template; - dvr_thread_params args; - args.filename_template = tpl; - args.mp4_fragmentation_mode = mp4_fragmentation_mode; - args.dvr_filenames_with_sequence = dvr_filenames_with_sequence; - args.video_framerate = reenc_params.fps; - args.max_file_size = dvr_max_file_size; - uint32_t rw, rh; reenc_target_dims(rw, rh); - args.video_p.video_frm_width = rw; - args.video_p.video_frm_height = rh; - args.video_p.codec = reenc_params.codec; - dvr_reenc_inst = new Dvr(args); - pthread_create(&g_tid_dvr_reenc, NULL, &Dvr::__THREAD__, dvr_reenc_inst); - reencoder = new MppEncoder(reenc_params, [](std::shared_ptr> nal) { - if (dvr_enabled && dvr_reenc_inst) dvr_reenc_inst->frame(nal); + if (dvr_enabled && receiver) receiver->dvr_reenc_push(nal); }); pthread_create(&g_tid_enc, NULL, &MppEncoder::__THREAD__, reencoder); frame_proc = new FrameProcessor(reencoder, reenc_params.fps, reenc_params.resolution, drm_fd, @@ -813,10 +805,8 @@ extern "C" { frame_proc->set_color_correction(live_colortrans_gain, live_colortrans_offset, drm_fd); pthread_create(&g_tid_fproc, NULL, &FrameProcessor::__THREAD__, frame_proc); - dvr_reenc_inst->on_start_cb = []() { - if (reencoder) reencoder->request_idr(); - if (frame_proc) frame_proc->clear_fatal_error(); - }; + dvr_configure_reenc_receiver(); + if (receiver) receiver->set_dvr_reenc_on_start(dvr_reenc_on_start); } dvr_mode = new_mode; @@ -853,7 +843,22 @@ bool feed_packet_to_decoder(MppPacket *packet,void* data_p,int data_len){ return true; } -std::unique_ptr receiver; +// Runtime audio on/off for the OSD menu (System -> Receiver -> Audio). +extern "C" { + int audio_get_enabled(void) { + return (receiver && receiver->get_audio_enabled()) ? 1 : 0; + } + void audio_set_enabled(int enabled) { + if (receiver) receiver->set_audio_enabled(enabled != 0); + } + void audio_set_device(const char* device) { + if (receiver) receiver->set_audio_device(device ? device : ""); + } + void audio_set_volume(int percent) { + if (receiver) receiver->set_audio_volume(percent / 100.0); + } +} + static MppCodingType current_mpp_type = MPP_VIDEO_CodingHEVC; static MppCodingType stream_mpp_type = MPP_VIDEO_CodingHEVC; @@ -1069,6 +1074,7 @@ void read_gstreamerpipe_stream(MppPacket *packet, int gst_udp_port, const char * } else { receiver = std::make_unique(gst_udp_port, codec); } + receiver->configure_audio(audio_enabled, audio_device, audio_pt, audio_volume / 100.0); // Realign the MPP decoder whenever the receiver detects a mid-stream codec // switch and rebuilds its pipeline. receiver->set_codec_changed_callback([](VideoCodec c) { @@ -1105,10 +1111,19 @@ void read_gstreamerpipe_stream(MppPacket *packet, int gst_udp_port, const char * } else { stall_count = 0; } - if (dvr_enabled && dvr_raw != NULL) { - dvr_raw->frame(frame); - } + // Raw DVR is no longer fed frame-by-frame here: the receiver records the + // native stream (+ muxed Opus) in-pipeline via splitmuxsink. }; + // Hand the receiver its raw-DVR naming policy + split size, then mirror the + // current record intent (e.g. --dvr-start autostart) before the pull thread + // begins driving the record branch. + dvr_configure_receiver(); + dvr_configure_reenc_receiver(); + if (receiver) { + receiver->set_dvr_reenc_on_start(dvr_reenc_on_start); + receiver->dvr_request_recording(dvr_enabled != 0 && dvr_mode_has_raw()); + receiver->dvr_reenc_request_recording(dvr_enabled != 0 && dvr_mode_has_reenc()); + } receiver->start_receiving(cb); // In auto mode the global codec stays UNKNOWN through receiver construction // (that sentinel is how the receiver knows to auto-detect). Now that the @@ -1196,6 +1211,14 @@ void printHelp() { "\n" " --codec - Video codec, should be the same as on VTX (Default: h265 )\n" "\n" + " --audio - Play Opus audio muxed into the RTP stream (same port, by payload type)\n" + "\n" + " --audio-device - Audio output: ALSA card id (e.g. rockchiphdmi) or device string (Default: system default)\n" + "\n" + " --audio-pt - RTP payload type carrying the Opus audio (Default: 98)\n" + "\n" + " --audio-volume - Audio output volume in percent 0-100 (Default: 100)\n" + "\n" " --log-level - Log verbosity level, debug|info|warn|error (Default: info)\n" "\n" " --log-file - Also log to (rotated at 1MB, 3 files kept).\n" @@ -1218,7 +1241,7 @@ void printHelp() { "\n" " --dvr-start - Start DVR immediately\n" "\n" - " --dvr-framerate - Force the dvr framerate for smoother dvr, ex: 60\n" + " --dvr-framerate - [DEPRECATED, ignored] DVR now uses the stream's own timestamps\n" "\n" " --dvr-max-size - Split DVR files at megabytes (Default: 4000, for VFAT)\n" "\n" @@ -1324,6 +1347,26 @@ int main(int argc, char **argv) continue; } + __OnArgument("--audio") { + audio_enabled = true; + continue; + } + + __OnArgument("--audio-device") { + audio_device = __ArgValue; + continue; + } + + __OnArgument("--audio-pt") { + audio_pt = atoi(__ArgValue); + continue; + } + + __OnArgument("--audio-volume") { + audio_volume = atoi(__ArgValue); + continue; + } + __OnArgument("--dvr-start") { dvr_autostart = 1; continue; @@ -1340,7 +1383,12 @@ int main(int argc, char **argv) } __OnArgument("--dvr-framerate") { - video_framerate = atoi(__ArgValue); + // Deprecated: kept only so existing launch scripts don't error. The DVR + // now muxes the native stream on its own timestamps (no fixed-framerate + // re-stamping), so this value is ignored. + video_framerate = atoi(__ArgValue); // consume the value token + spdlog::warn("--dvr-framerate is deprecated and ignored; the DVR uses the " + "stream's own timestamps. It will be removed in a future release."); continue; } @@ -1572,11 +1620,9 @@ int main(int argc, char **argv) } idr_set_enabled(!disable_gregidr); - if (dvr_template != NULL && (dvr_mode == DVR_MODE_RAW || dvr_mode == DVR_MODE_BOTH) && video_framerate < 0) { - printf("--dvr-framerate must be provided when raw DVR is enabled.\n" - "Use --dvr-mode reencode with --dvr-reenc-fps for hardware re-encoding only.\n"); - return 0; - } + // (--dvr-framerate is no longer required for raw DVR: the splitmuxsink + // recorder muxes the native stream on its own timestamps, so no fixed + // framerate is needed. The flag is still accepted but ignored for raw.) printf("PixelPilot Rockchip %d.%d\n", APP_VERSION_MAJOR, APP_VERSION_MINOR); @@ -1784,46 +1830,15 @@ int main(int argc, char **argv) pthread_t tid_frame, tid_display, tid_osd, tid_mavlink, tid_wfbcli; if (dvr_template != NULL) { - bool has_raw = (dvr_mode == DVR_MODE_RAW || dvr_mode == DVR_MODE_BOTH); bool has_reenc = (dvr_mode == DVR_MODE_REENCODE || dvr_mode == DVR_MODE_BOTH); - bool both = (dvr_mode == DVR_MODE_BOTH); - - if (has_raw) { - dvr_thread_params args; - char *tpl = both ? dvr_template_with_suffix(dvr_template, "_raw") : dvr_template; - args.filename_template = tpl; - args.mp4_fragmentation_mode = mp4_fragmentation_mode; - args.dvr_filenames_with_sequence = dvr_filenames_with_sequence; - args.video_framerate = video_framerate; - args.max_file_size = dvr_max_file_size; - args.video_p.video_frm_width = output_list->video_frm_width; - args.video_p.video_frm_height = output_list->video_frm_height; - args.video_p.codec = codec; - dvr_raw = new Dvr(args); - ret = pthread_create(&g_tid_dvr_raw, NULL, &Dvr::__THREAD__, dvr_raw); - assert(!ret); - } + // Neither recorder needs a muxer object here — both live inside the + // receiver's pipeline (configured in read_gstreamerpipe_stream()). We only + // spin up the MPP encoder + frame pacer that feed the reenc recorder; the + // encoder output is pushed to the receiver via dvr_reenc_push(). if (has_reenc) { - dvr_thread_params args; - char *tpl = both ? dvr_template_with_suffix(dvr_template, "_reenc") : dvr_template; - args.filename_template = tpl; - args.mp4_fragmentation_mode = mp4_fragmentation_mode; - args.dvr_filenames_with_sequence = dvr_filenames_with_sequence; - args.video_framerate = reenc_params.fps; - args.max_file_size = dvr_max_file_size; - uint32_t rw, rh; reenc_target_dims(rw, rh); - args.video_p.video_frm_width = rw; - args.video_p.video_frm_height = rh; - args.video_p.codec = reenc_params.codec; - dvr_reenc_inst = new Dvr(args); - ret = pthread_create(&g_tid_dvr_reenc, NULL, &Dvr::__THREAD__, dvr_reenc_inst); - assert(!ret); - reencoder = new MppEncoder(reenc_params, [](std::shared_ptr> nal) { - if (dvr_enabled && dvr_reenc_inst != NULL) { - dvr_reenc_inst->frame(nal); - } + if (dvr_enabled && receiver) receiver->dvr_reenc_push(nal); }); ret = pthread_create(&g_tid_enc, NULL, &MppEncoder::__THREAD__, reencoder); assert(!ret); @@ -1837,22 +1852,20 @@ int main(int argc, char **argv) } ret = pthread_create(&g_tid_fproc, NULL, &FrameProcessor::__THREAD__, frame_proc); assert(!ret); - dvr_reenc_inst->on_start_cb = []() { - if (reencoder) reencoder->request_idr(); - if (frame_proc) frame_proc->clear_fatal_error(); - }; + // on_start hook is wired to the receiver in read_gstreamerpipe_stream(), + // which is where the reenc recorder itself gets configured. spdlog::info("Re-encoding recorder: codec={} fps={} bitrate={}kbps", reenc_params.codec == VideoCodec::H265 ? "h265" : "h264", reenc_params.fps, reenc_params.bitrate_kbps); } if (dvr_autostart) { + // Just set intent here; the receiver doesn't exist yet. Both recorders + // are armed from dvr_enabled in read_gstreamerpipe_stream(). spdlog::info("DVR: autostart (--dvr-start) recording (mode={})", dvr_mode == DVR_MODE_RAW ? "raw" : dvr_mode == DVR_MODE_REENCODE ? "reencode" : "both"); dvr_enabled = 1; osd_publish_bool_fact("dvr.recording", NULL, 0, true); - if (dvr_raw) dvr_raw->start_recording(); - if (dvr_reenc_inst) dvr_reenc_inst->start_recording(); if (reencoder) reencoder->request_idr(); } } @@ -1932,14 +1945,6 @@ int main(int argc, char **argv) ret = pthread_join(g_tid_enc, NULL); assert(!ret); } - if (g_tid_dvr_raw) { - ret = pthread_join(g_tid_dvr_raw, NULL); - assert(!ret); - } - if (g_tid_dvr_reenc) { - ret = pthread_join(g_tid_dvr_reenc, NULL); - assert(!ret); - } } ret = mpi.mpi->reset(mpi.ctx); diff --git a/src/menu.h b/src/menu.h index 80939b61..81df5c6f 100644 --- a/src/menu.h +++ b/src/menu.h @@ -23,4 +23,8 @@ void toggle_rec_enabled(void); /* Query current recording state (for menu sync). */ int menu_is_recording(void); +/* Opus audio on/off (System -> Receiver -> Audio). Defined in main.cpp. */ +// int audio_get_enabled(void); +// void audio_set_enabled(int enabled); + void pp_menu_main(void); \ No newline at end of file diff --git a/src/minimp4.h b/src/minimp4.h deleted file mode 100644 index cfae2bd1..00000000 --- a/src/minimp4.h +++ /dev/null @@ -1,3501 +0,0 @@ -#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 . -*/ - -#include -#include -#include -#include -#include -#include - -#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 0 - -// 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 - - - -#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 to - // + 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, int duration) -{ - 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, duration, sample_kind); - free(tmp); - } - break; - } - return err; -} - -uint64_t last_ms = 0; - -int mp4_h26x_write_nal(mp4_h26x_writer_t *h, const unsigned char *nal, int length, int 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 diff --git a/src/simulator.c b/src/simulator.c index 93904836..2e3240de 100644 --- a/src/simulator.c +++ b/src/simulator.c @@ -41,6 +41,10 @@ void my_log_cb(lv_log_level_t level, const char * buf) // which is not part of the simulator build) bool restream_get_enabled() { return false; } void restream_set_enabled(bool enabled) { (void)enabled; } +int audio_get_enabled(void) { return 0; } +void audio_set_enabled(int enabled) { (void)enabled; } +void audio_set_device(const char* device) { (void)device; } +void audio_set_volume(int percent) { (void)percent; } void restream_scan_clients(char* buf, size_t buf_len) { if (buf && buf_len) buf[0] = '\0'; } const char* restream_get_manual_ip() { return ""; } void restream_set_manual_ip(const char* ip) { (void)ip; }