#include "clip_recorder.h" #include "faststart.h" #include #include #include #include #include #include #include #include #define TAG "ClipRecorder" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) void ClipRecorder::Configure(int width, int height, uint32_t audioSampleRate, uint32_t audioChannels_, int audioBitrate_) { std::lock_guard lock(mutex); videoWidth = width; videoHeight = height; sampleRate = audioSampleRate; audioChannels = audioChannels_; audioBitrate = audioBitrate_; LOGI("Configured: %dx%d, audio %uHz %uch %dbps", width, height, audioSampleRate, audioChannels_, audioBitrate_); } void ClipRecorder::SetVideoFormat(const uint8_t* codecConfig, uint32_t size) { std::lock_guard lock(mutex); videoCodecConfig.assign(codecConfig, codecConfig + size); LOGI("Video codec config set: %u bytes", size); } void ClipRecorder::SetAudioFormat(const uint8_t* codecConfig, uint32_t size) { std::lock_guard lock(mutex); audioCodecConfig.assign(codecConfig, codecConfig + size); LOGI("Audio codec config set: %u bytes", size); } void ClipRecorder::Start() { std::lock_guard lock(mutex); gopBuffer.clear(); currentGop = GopBuffer(); audioBuffer.clear(); active = true; LOGI("Started"); } void ClipRecorder::Stop() { std::lock_guard lock(mutex); active = false; gopBuffer.clear(); currentGop = GopBuffer(); audioBuffer.clear(); videoCodecConfig.clear(); audioCodecConfig.clear(); LOGI("Stopped"); } void ClipRecorder::FeedVideoPacket(const uint8_t* data, uint32_t size, int64_t timestampUs, bool isKeyframe) { std::lock_guard lock(mutex); if (!active) return; if (isKeyframe) { // Finalize current GOP and push to buffer if (!currentGop.samples.empty()) { gopBuffer.push_back(std::move(currentGop)); } currentGop = GopBuffer(); currentGop.startTimeUs = timestampUs; } VideoSample sample; sample.data.assign(data, data + size); sample.timestampUs = timestampUs; sample.isKeyframe = isKeyframe; currentGop.samples.push_back(std::move(sample)); TrimBuffers(); } void ClipRecorder::FeedAudioPacket(const uint8_t* data, uint32_t size, int64_t timestampUs) { std::lock_guard lock(mutex); if (!active) return; AudioSample sample; sample.data.assign(data, data + size); sample.timestampUs = timestampUs; audioBuffer.push_back(std::move(sample)); TrimBuffers(); } void ClipRecorder::TrimBuffers() { // Trim oldest GOPs to stay under MAX_BUFFER_DURATION_US if (gopBuffer.size() < 2) return; int64_t newestTs = 0; if (!currentGop.samples.empty()) { newestTs = currentGop.samples.back().timestampUs; } else if (!gopBuffer.empty() && !gopBuffer.back().samples.empty()) { newestTs = gopBuffer.back().samples.back().timestampUs; } if (newestTs == 0) return; while (!gopBuffer.empty()) { int64_t oldestTs = gopBuffer.front().startTimeUs; if (newestTs - oldestTs > MAX_BUFFER_DURATION_US && gopBuffer.size() > 1) { gopBuffer.pop_front(); } else { break; } } // Trim audio samples older than oldest remaining video if (!gopBuffer.empty()) { int64_t videoStart = gopBuffer.front().startTimeUs; while (!audioBuffer.empty() && audioBuffer.front().timestampUs < videoStart) { audioBuffer.pop_front(); } } } bool ClipRecorder::FlushClip(const std::string& outputDir) { // Copy data under lock std::vector gopsCopy; std::vector audioCopy; std::vector videoCsdCopy; std::vector audioCsdCopy; int w, h; uint32_t sr, ch; int abr; { std::lock_guard lock(mutex); if (!active) return false; if (gopBuffer.empty()) { LOGW("FlushClip: no complete GOPs"); return false; } if (videoCodecConfig.empty()) { LOGW("FlushClip: no video codec config"); return false; } gopsCopy.assign(gopBuffer.begin(), gopBuffer.end()); audioCopy.assign(audioBuffer.begin(), audioBuffer.end()); videoCsdCopy = videoCodecConfig; audioCsdCopy = audioCodecConfig; w = videoWidth; h = videoHeight; sr = sampleRate; ch = audioChannels; abr = audioBitrate; } // Mux to temp file std::string tempPath = outputDir + "/preview_clip_tmp.mp4"; std::string finalPath = outputDir + "/preview_clip.mp4"; int fd = open(tempPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) { LOGE("FlushClip: failed to open temp file: %s", tempPath.c_str()); return false; } AMediaMuxer* muxer = AMediaMuxer_new(fd, AMEDIAMUXER_OUTPUT_FORMAT_MPEG_4); if (!muxer) { LOGE("FlushClip: failed to create muxer"); close(fd); return false; } // Video track — split Annex-B SPS+PPS into separate csd-0 (SPS) and csd-1 (PPS) // MediaCodec CODEC_CONFIG output: [00 00 00 01 SPS] [00 00 00 01 PPS] // AMediaMuxer expects csd-0 = SPS NAL (with start code), csd-1 = PPS NAL (with start code) const uint8_t* csd = videoCsdCopy.data(); size_t csdSize = videoCsdCopy.size(); size_t spsLen = csdSize; size_t ppsOffset = 0; size_t ppsLen = 0; // Find the second start code (00 00 00 01) to split SPS from PPS for (size_t i = 4; i + 3 < csdSize; i++) { if (csd[i] == 0x00 && csd[i+1] == 0x00 && csd[i+2] == 0x00 && csd[i+3] == 0x01) { spsLen = i; ppsOffset = i; ppsLen = csdSize - i; break; } } LOGI("FlushClip: csd total=%zu, SPS=%zu bytes, PPS=%zu bytes", csdSize, spsLen, ppsLen); AMediaFormat* videoFormat = AMediaFormat_new(); AMediaFormat_setString(videoFormat, AMEDIAFORMAT_KEY_MIME, "video/avc"); AMediaFormat_setInt32(videoFormat, AMEDIAFORMAT_KEY_WIDTH, w); AMediaFormat_setInt32(videoFormat, AMEDIAFORMAT_KEY_HEIGHT, h); AMediaFormat_setBuffer(videoFormat, "csd-0", csd, spsLen); if (ppsLen > 0) { AMediaFormat_setBuffer(videoFormat, "csd-1", csd + ppsOffset, ppsLen); } ssize_t videoTrack = AMediaMuxer_addTrack(muxer, videoFormat); AMediaFormat_delete(videoFormat); if (videoTrack < 0) { LOGE("FlushClip: failed to add video track"); AMediaMuxer_delete(muxer); close(fd); return false; } // Audio track (optional — may not have audio config) ssize_t audioTrack = -1; if (!audioCsdCopy.empty()) { AMediaFormat* audioFormat = AMediaFormat_new(); AMediaFormat_setString(audioFormat, AMEDIAFORMAT_KEY_MIME, "audio/mp4a-latm"); AMediaFormat_setInt32(audioFormat, AMEDIAFORMAT_KEY_SAMPLE_RATE, sr); AMediaFormat_setInt32(audioFormat, AMEDIAFORMAT_KEY_CHANNEL_COUNT, ch); AMediaFormat_setInt32(audioFormat, AMEDIAFORMAT_KEY_BIT_RATE, abr); AMediaFormat_setBuffer(audioFormat, "csd-0", audioCsdCopy.data(), audioCsdCopy.size()); audioTrack = AMediaMuxer_addTrack(muxer, audioFormat); AMediaFormat_delete(audioFormat); if (audioTrack < 0) { LOGW("FlushClip: failed to add audio track, continuing video-only"); audioTrack = -1; } } media_status_t status = AMediaMuxer_start(muxer); if (status != AMEDIA_OK) { LOGE("FlushClip: failed to start muxer: %d", status); AMediaMuxer_delete(muxer); close(fd); return false; } // Compute base timestamp for zero-based output int64_t baseTs = gopsCopy.front().startTimeUs; // Write video samples int videoSamplesWritten = 0; for (const auto& gop : gopsCopy) { for (const auto& sample : gop.samples) { AMediaCodecBufferInfo info; info.offset = 0; info.size = static_cast(sample.data.size()); info.presentationTimeUs = sample.timestampUs - baseTs; info.flags = sample.isKeyframe ? AMEDIACODEC_BUFFER_FLAG_KEY_FRAME : 0; AMediaMuxer_writeSampleData(muxer, videoTrack, sample.data.data(), &info); videoSamplesWritten++; } } // Write audio samples int audioSamplesWritten = 0; if (audioTrack >= 0) { for (const auto& sample : audioCopy) { // Only include audio within the video time range int64_t relTs = sample.timestampUs - baseTs; if (relTs < 0) continue; AMediaCodecBufferInfo info; info.offset = 0; info.size = static_cast(sample.data.size()); info.presentationTimeUs = relTs; info.flags = 0; AMediaMuxer_writeSampleData(muxer, audioTrack, sample.data.data(), &info); audioSamplesWritten++; } } AMediaMuxer_stop(muxer); AMediaMuxer_delete(muxer); close(fd); LOGI("FlushClip: muxed %d video + %d audio samples to temp file", videoSamplesWritten, audioSamplesWritten); // Apply faststart (move moov atom to front) if (!MoovFastStart(tempPath, finalPath)) { LOGW("FlushClip: faststart failed, using non-optimized file"); rename(tempPath.c_str(), finalPath.c_str()); } else { unlink(tempPath.c_str()); } LOGI("FlushClip: clip ready at %s", finalPath.c_str()); if (clipReadyCallback) { clipReadyCallback(finalPath); } return true; } void ClipRecorder::SetClipReadyCallback(ClipReadyCallback cb) { std::lock_guard lock(mutex); clipReadyCallback = std::move(cb); }