You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							234 lines
						
					
					
						
							9.1 KiB
						
					
					
				
			
		
		
	
	
							234 lines
						
					
					
						
							9.1 KiB
						
					
					
				| #include <cassert>
 | |
| 
 | |
| #include "system/loggerd/video_writer.h"
 | |
| #include "common/swaglog.h"
 | |
| #include "common/util.h"
 | |
| 
 | |
| VideoWriter::VideoWriter(const char *path, const char *filename, bool remuxing, int width, int height, int fps, cereal::EncodeIndex::Type codec)
 | |
|   : remuxing(remuxing) {
 | |
|   vid_path = util::string_format("%s/%s", path, filename);
 | |
|   lock_path = util::string_format("%s/%s.lock", path, filename);
 | |
| 
 | |
|   int lock_fd = HANDLE_EINTR(open(lock_path.c_str(), O_RDWR | O_CREAT, 0664));
 | |
|   assert(lock_fd >= 0);
 | |
|   close(lock_fd);
 | |
| 
 | |
|   LOGD("encoder_open %s remuxing:%d", this->vid_path.c_str(), this->remuxing);
 | |
|   if (this->remuxing) {
 | |
|     bool raw = (codec == cereal::EncodeIndex::Type::BIG_BOX_LOSSLESS);
 | |
|     avformat_alloc_output_context2(&this->ofmt_ctx, NULL, raw ? "matroska" : NULL, this->vid_path.c_str());
 | |
|     assert(this->ofmt_ctx);
 | |
| 
 | |
|     // set codec correctly. needed?
 | |
|     assert(codec != cereal::EncodeIndex::Type::FULL_H_E_V_C);
 | |
|     const AVCodec *avcodec = avcodec_find_encoder(raw ? AV_CODEC_ID_FFVHUFF : AV_CODEC_ID_H264);
 | |
|     assert(avcodec);
 | |
| 
 | |
|     this->codec_ctx = avcodec_alloc_context3(avcodec);
 | |
|     assert(this->codec_ctx);
 | |
|     this->codec_ctx->width = width;
 | |
|     this->codec_ctx->height = height;
 | |
|     this->codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
 | |
|     this->codec_ctx->time_base = (AVRational){ 1, fps };
 | |
| 
 | |
|     if (codec == cereal::EncodeIndex::Type::BIG_BOX_LOSSLESS) {
 | |
|       // without this, there's just noise
 | |
|       int err = avcodec_open2(this->codec_ctx, avcodec, NULL);
 | |
|       assert(err >= 0);
 | |
|     }
 | |
| 
 | |
|     this->out_stream = avformat_new_stream(this->ofmt_ctx, raw ? avcodec : NULL);
 | |
|     assert(this->out_stream);
 | |
| 
 | |
|     int err = avio_open(&this->ofmt_ctx->pb, this->vid_path.c_str(), AVIO_FLAG_WRITE);
 | |
|     assert(err >= 0);
 | |
| 
 | |
|   } else {
 | |
|     this->of = util::safe_fopen(this->vid_path.c_str(), "wb");
 | |
|     assert(this->of);
 | |
|   }
 | |
| }
 | |
| 
 | |
| void VideoWriter::initialize_audio(int sample_rate) {
 | |
|   assert(this->ofmt_ctx->oformat->audio_codec != AV_CODEC_ID_NONE); // check output format supports audio streams
 | |
|   const AVCodec *audio_avcodec = avcodec_find_encoder(AV_CODEC_ID_AAC);
 | |
|   assert(audio_avcodec);
 | |
|   this->audio_codec_ctx = avcodec_alloc_context3(audio_avcodec);
 | |
|   assert(this->audio_codec_ctx);
 | |
|   this->audio_codec_ctx->sample_fmt = AV_SAMPLE_FMT_FLTP;
 | |
|   this->audio_codec_ctx->sample_rate = sample_rate;
 | |
|   #if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 28, 100)  // FFmpeg 5.1+
 | |
|   av_channel_layout_default(&this->audio_codec_ctx->ch_layout, 1);
 | |
|   #else
 | |
|   this->audio_codec_ctx->channel_layout = AV_CH_LAYOUT_MONO;
 | |
|   #endif
 | |
|   this->audio_codec_ctx->bit_rate = 32000;
 | |
|   this->audio_codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
 | |
|   this->audio_codec_ctx->time_base = (AVRational){1, audio_codec_ctx->sample_rate};
 | |
|   int err = avcodec_open2(this->audio_codec_ctx, audio_avcodec, NULL);
 | |
|   assert(err >= 0);
 | |
|   av_log_set_level(AV_LOG_WARNING); // hide "QAvg" info msgs at the end of every segment
 | |
| 
 | |
|   this->audio_stream = avformat_new_stream(this->ofmt_ctx, NULL);
 | |
|   assert(this->audio_stream);
 | |
|   err = avcodec_parameters_from_context(this->audio_stream->codecpar, this->audio_codec_ctx);
 | |
|   assert(err >= 0);
 | |
| 
 | |
|   this->audio_frame = av_frame_alloc();
 | |
|   assert(this->audio_frame);
 | |
|   this->audio_frame->format = this->audio_codec_ctx->sample_fmt;
 | |
|   #if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 28, 100)  // FFmpeg 5.1+
 | |
|   av_channel_layout_copy(&this->audio_frame->ch_layout, &this->audio_codec_ctx->ch_layout);
 | |
|   #else
 | |
|   this->audio_frame->channel_layout = this->audio_codec_ctx->channel_layout;
 | |
|   #endif
 | |
|   this->audio_frame->sample_rate = this->audio_codec_ctx->sample_rate;
 | |
|   this->audio_frame->nb_samples = this->audio_codec_ctx->frame_size;
 | |
|   err = av_frame_get_buffer(this->audio_frame, 0);
 | |
|   assert(err >= 0);
 | |
| }
 | |
| 
 | |
| void VideoWriter::write(uint8_t *data, int len, long long timestamp, bool codecconfig, bool keyframe) {
 | |
|   if (of && data) {
 | |
|     size_t written = util::safe_fwrite(data, 1, len, of);
 | |
|     if (written != len) {
 | |
|       LOGE("failed to write file.errno=%d", errno);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (remuxing) {
 | |
|     if (codecconfig) {
 | |
|       if (len > 0) {
 | |
|         codec_ctx->extradata = (uint8_t*)av_mallocz(len + AV_INPUT_BUFFER_PADDING_SIZE);
 | |
|         codec_ctx->extradata_size = len;
 | |
|         memcpy(codec_ctx->extradata, data, len);
 | |
|       }
 | |
|       int err = avcodec_parameters_from_context(out_stream->codecpar, codec_ctx);
 | |
|       assert(err >= 0);
 | |
|       // if there is an audio stream, it must be initialized before this point
 | |
|       err = avformat_write_header(ofmt_ctx, NULL);
 | |
|       assert(err >= 0);
 | |
|       header_written = true;
 | |
|     } else {
 | |
|       // input timestamps are in microseconds
 | |
|       AVRational in_timebase = {1, 1000000};
 | |
| 
 | |
|       AVPacket pkt = {};
 | |
|       pkt.data = data;
 | |
|       pkt.size = len;
 | |
|       pkt.stream_index = this->out_stream->index;
 | |
| 
 | |
|       enum AVRounding rnd = static_cast<enum AVRounding>(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
 | |
|       pkt.pts = pkt.dts = av_rescale_q_rnd(timestamp, in_timebase, ofmt_ctx->streams[0]->time_base, rnd);
 | |
|       pkt.duration = av_rescale_q(50*1000, in_timebase, ofmt_ctx->streams[0]->time_base);
 | |
| 
 | |
|       if (keyframe) {
 | |
|         pkt.flags |= AV_PKT_FLAG_KEY;
 | |
|       }
 | |
| 
 | |
|       // TODO: can use av_write_frame for non raw?
 | |
|       int err = av_interleaved_write_frame(ofmt_ctx, &pkt);
 | |
|       if (err < 0) { LOGW("ts encoder write issue len: %d ts: %lld", len, timestamp); }
 | |
| 
 | |
|       av_packet_unref(&pkt);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| void VideoWriter::write_audio(uint8_t *data, int len, long long timestamp, int sample_rate) {
 | |
|   if (!remuxing) return;
 | |
|   if (!audio_initialized) {
 | |
|     initialize_audio(sample_rate);
 | |
|     audio_initialized = true;
 | |
|   }
 | |
|   if (!audio_codec_ctx) return;
 | |
|   // sync logMonoTime of first audio packet with the timestampEof of first video packet
 | |
|   if (audio_pts == 0) {
 | |
|     audio_pts = (timestamp * audio_codec_ctx->sample_rate) / 1000000ULL;
 | |
|   }
 | |
| 
 | |
|   // convert s16le samples to fltp and add to buffer
 | |
|   const int16_t *raw_samples = reinterpret_cast<const int16_t*>(data);
 | |
|   int sample_count = len / sizeof(int16_t);
 | |
|   constexpr float normalizer = 1.0f / 32768.0f;
 | |
| 
 | |
|   const size_t max_buffer_size = sample_rate * 10; // 10 seconds
 | |
|   if (audio_buffer.size() + sample_count > max_buffer_size) {
 | |
|     size_t samples_to_drop = (audio_buffer.size() + sample_count) - max_buffer_size;
 | |
|     LOGE("Audio buffer overflow, dropping %zu oldest samples", samples_to_drop);
 | |
|     audio_buffer.erase(audio_buffer.begin(), audio_buffer.begin() + samples_to_drop);
 | |
|     audio_pts += samples_to_drop;
 | |
|   }
 | |
| 
 | |
|   // Add new samples to the buffer
 | |
|   const size_t original_size = audio_buffer.size();
 | |
|   audio_buffer.resize(original_size + sample_count);
 | |
|   std::transform(raw_samples, raw_samples + sample_count, audio_buffer.begin() + original_size,
 | |
|                 [](int16_t sample) { return sample * normalizer; });
 | |
| 
 | |
|   if (!header_written) return; // header not written yet, process audio frame after header is written
 | |
|   while (audio_buffer.size() >= audio_codec_ctx->frame_size) {
 | |
|     audio_frame->pts = audio_pts;
 | |
|     float *f_samples = reinterpret_cast<float*>(audio_frame->data[0]);
 | |
|     std::copy(audio_buffer.begin(), audio_buffer.begin() + audio_codec_ctx->frame_size, f_samples);
 | |
|     audio_buffer.erase(audio_buffer.begin(), audio_buffer.begin() + audio_codec_ctx->frame_size);
 | |
|     encode_and_write_audio_frame(audio_frame);
 | |
|   }
 | |
| }
 | |
| 
 | |
| void VideoWriter::encode_and_write_audio_frame(AVFrame* frame) {
 | |
|   if (!remuxing || !audio_codec_ctx) return;
 | |
|   int send_result = avcodec_send_frame(audio_codec_ctx, frame); // encode frame
 | |
|   if (send_result >= 0) {
 | |
|     AVPacket *pkt = av_packet_alloc();
 | |
|     while (avcodec_receive_packet(audio_codec_ctx, pkt) == 0) {
 | |
|       av_packet_rescale_ts(pkt, audio_codec_ctx->time_base, audio_stream->time_base);
 | |
|       pkt->stream_index = audio_stream->index;
 | |
| 
 | |
|       int err = av_interleaved_write_frame(ofmt_ctx, pkt); // write encoded frame
 | |
|       if (err < 0) {
 | |
|         LOGW("AUDIO: Write frame failed - error: %d", err);
 | |
|       }
 | |
|       av_packet_unref(pkt);
 | |
|     }
 | |
|     av_packet_free(&pkt);
 | |
|   } else {
 | |
|     LOGW("AUDIO: Failed to send audio frame to encoder: %d", send_result);
 | |
|   }
 | |
|   audio_pts += audio_codec_ctx->frame_size;
 | |
| }
 | |
| 
 | |
| void VideoWriter::process_remaining_audio() {
 | |
|   // Process remaining audio samples by padding with silence
 | |
|   if (audio_buffer.size() > 0 && audio_buffer.size() < audio_codec_ctx->frame_size) {
 | |
|     audio_buffer.resize(audio_codec_ctx->frame_size, 0.0f);
 | |
| 
 | |
|     // Encode final frame
 | |
|     audio_frame->pts = audio_pts;
 | |
|     float *f_samples = reinterpret_cast<float *>(audio_frame->data[0]);
 | |
|     std::copy(audio_buffer.begin(), audio_buffer.end(), f_samples);
 | |
|     encode_and_write_audio_frame(audio_frame);
 | |
|   }
 | |
| }
 | |
| 
 | |
| VideoWriter::~VideoWriter() {
 | |
|   if (this->remuxing) {
 | |
|     if (this->audio_codec_ctx) {
 | |
|       process_remaining_audio();
 | |
|       encode_and_write_audio_frame(NULL); // flush encoder
 | |
|       avcodec_free_context(&this->audio_codec_ctx);
 | |
|     }
 | |
|     int err = av_write_trailer(this->ofmt_ctx);
 | |
|     if (err != 0) LOGE("av_write_trailer failed %d", err);
 | |
|     avcodec_free_context(&this->codec_ctx);
 | |
|     if (this->audio_frame) av_frame_free(&this->audio_frame);
 | |
|     err = avio_closep(&this->ofmt_ctx->pb);
 | |
|     if (err != 0) LOGE("avio_closep failed %d", err);
 | |
|     avformat_free_context(this->ofmt_ctx);
 | |
|   } else {
 | |
|     util::safe_fflush(this->of);
 | |
|     fclose(this->of);
 | |
|     this->of = nullptr;
 | |
|   }
 | |
|   unlink(this->lock_path.c_str());
 | |
| }
 | |
| 
 |