/* * Copyright 2009-2010, Stephan Aßmus * Copyright 2018, Dario Casalinuovo * All rights reserved. Distributed under the terms of the GNU L-GPL license. */ #include "AVFormatWriter.h" #include #include #include #include #include #include #include #include #include #include #include #include extern "C" { #include "avformat.h" } #include "DemuxerTable.h" #include "EncoderTable.h" #include "gfx_util.h" //#define TRACE_AVFORMAT_WRITER #ifdef TRACE_AVFORMAT_WRITER # define TRACE printf # define TRACE_IO(a...) # define TRACE_PACKET printf #else # define TRACE(a...) # define TRACE_IO(a...) # define TRACE_PACKET(a...) #endif #define ERROR(a...) fprintf(stderr, a) static const size_t kIOBufferSize = 64 * 1024; // TODO: This could depend on the BMediaFile creation flags, IIRC, // they allow to specify a buffering mode. typedef AVCodecID CodecID; // #pragma mark - AVFormatWriter::StreamCookie class AVFormatWriter::StreamCookie { public: StreamCookie(AVFormatContext* context, BLocker* streamLock); virtual ~StreamCookie(); status_t Init(media_format* format, const media_codec_info* codecInfo); status_t WriteChunk(const void* chunkBuffer, size_t chunkSize, media_encode_info* encodeInfo); status_t AddTrackInfo(uint32 code, const void* data, size_t size, uint32 flags); private: AVFormatContext* fFormatContext; AVStream* fStream; AVPacket* fPacket; // Since different threads may write to the target, // we need to protect the file position and I/O by a lock. BLocker* fStreamLock; }; AVFormatWriter::StreamCookie::StreamCookie(AVFormatContext* context, BLocker* streamLock) : fFormatContext(context), fStream(NULL), fStreamLock(streamLock) { fPacket = av_packet_alloc(); } AVFormatWriter::StreamCookie::~StreamCookie() { // fStream is freed automatically when the codec context is closed av_packet_free(&fPacket); } static void set_channel_count(AVCodecParameters* context, int count) { #if LIBAVCODEC_VERSION_MAJOR >= 60 context->ch_layout.nb_channels = count; #else context->channels = count; #endif } status_t AVFormatWriter::StreamCookie::Init(media_format* format, const media_codec_info* codecInfo) { TRACE("AVFormatWriter::StreamCookie::Init()\n"); BAutolock _(fStreamLock); fPacket->stream_index = fFormatContext->nb_streams; fStream = avformat_new_stream(fFormatContext, NULL); if (fStream == NULL) { TRACE(" failed to add new stream\n"); return B_ERROR; } fStream->id = fPacket->stream_index; // TRACE(" fStream->codecpar: %p\n", fStream->codecpar); // TODO: This is a hack for now! Use avcodec_find_encoder_by_name() // or something similar... fStream->codecpar->codec_id = (CodecID)codecInfo->sub_id; if (fStream->codecpar->codec_id == AV_CODEC_ID_NONE) fStream->codecpar->codec_id = raw_audio_codec_id_for(*format); // Setup the stream according to the media format... if (format->type == B_MEDIA_RAW_VIDEO) { fStream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; fStream->time_base.den = (int)format->u.raw_video.field_rate; fStream->time_base.num = 1; // video size fStream->codecpar->width = format->u.raw_video.display.line_width; fStream->codecpar->height = format->u.raw_video.display.line_count; // pixel aspect ratio fStream->sample_aspect_ratio.num = format->u.raw_video.pixel_width_aspect; fStream->sample_aspect_ratio.den = format->u.raw_video.pixel_height_aspect; if (fStream->sample_aspect_ratio.num == 0 || fStream->sample_aspect_ratio.den == 0) { av_reduce(&fStream->sample_aspect_ratio.num, &fStream->sample_aspect_ratio.den, fStream->codecpar->width, fStream->codecpar->height, 255); } fStream->codecpar->sample_aspect_ratio = fStream->sample_aspect_ratio; // Use the last supported pixel format of the AVCodec, which we hope // is the one with the best quality (true for all currently supported // encoders). // AVCodec* codec = fStream->codecpar->codec; // for (int i = 0; codec->pix_fmts[i] != PIX_FMT_NONE; i++) // fStream->codecpar->pix_fmt = codec->pix_fmts[i]; fStream->codecpar->format = AV_PIX_FMT_YUV420P; } else if (format->type == B_MEDIA_RAW_AUDIO) { fStream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO; // frame rate fStream->codecpar->sample_rate = (int)format->u.raw_audio.frame_rate; // channels set_channel_count(fStream->codecpar, format->u.raw_audio.channel_count); // set fStream to the audio format we want to use. This is only a hint // (each encoder has a different set of accepted formats) switch (format->u.raw_audio.format) { case media_raw_audio_format::B_AUDIO_FLOAT: fStream->codecpar->format = AV_SAMPLE_FMT_FLT; break; case media_raw_audio_format::B_AUDIO_DOUBLE: fStream->codecpar->format = AV_SAMPLE_FMT_DBL; break; case media_raw_audio_format::B_AUDIO_INT: fStream->codecpar->format = AV_SAMPLE_FMT_S32; break; case media_raw_audio_format::B_AUDIO_SHORT: fStream->codecpar->format = AV_SAMPLE_FMT_S16; break; case media_raw_audio_format::B_AUDIO_UCHAR: fStream->codecpar->format = AV_SAMPLE_FMT_U8; break; case media_raw_audio_format::B_AUDIO_CHAR: default: return B_MEDIA_BAD_FORMAT; break; } // Now negociate the actual format with the encoder // First check if the requested format is acceptable const AVCodec* codec = avcodec_find_encoder(fStream->codecpar->codec_id); if (codec == NULL) return B_MEDIA_BAD_FORMAT; const enum AVSampleFormat *p = codec->sample_fmts; for (; *p != -1; p++) { if (*p == fStream->codecpar->format) break; } // If not, force one of the acceptable ones if (*p == -1) { fStream->codecpar->format = codec->sample_fmts[0]; // And finally set the format struct to the accepted format. It is // then up to the caller to make sure we get data matching that // format. switch (fStream->codecpar->format) { case AV_SAMPLE_FMT_FLT: format->u.raw_audio.format = media_raw_audio_format::B_AUDIO_FLOAT; break; case AV_SAMPLE_FMT_DBL: format->u.raw_audio.format = media_raw_audio_format::B_AUDIO_DOUBLE; break; case AV_SAMPLE_FMT_S32: format->u.raw_audio.format = media_raw_audio_format::B_AUDIO_INT; break; case AV_SAMPLE_FMT_S16: format->u.raw_audio.format = media_raw_audio_format::B_AUDIO_SHORT; break; case AV_SAMPLE_FMT_U8: format->u.raw_audio.format = media_raw_audio_format::B_AUDIO_UCHAR; break; default: return B_MEDIA_BAD_FORMAT; break; } } #if LIBAVCODEC_VERSION_MAJOR >= 60 if (format->u.raw_audio.channel_mask == 0) { // guess the channel mask... av_channel_layout_default(&fStream->codecpar->ch_layout, format->u.raw_audio.channel_count); } else { // The bits match 1:1 for media_multi_channels and FFmpeg defines. av_channel_layout_from_mask(&fStream->codecpar->ch_layout, format->u.raw_audio.channel_mask); } #else if (format->u.raw_audio.channel_mask == 0) { // guess the channel mask... switch (format->u.raw_audio.channel_count) { default: case 2: fStream->codecpar->channel_layout = AV_CH_LAYOUT_STEREO; break; case 1: fStream->codecpar->channel_layout = AV_CH_LAYOUT_MONO; break; case 3: fStream->codecpar->channel_layout = AV_CH_LAYOUT_SURROUND; break; case 4: fStream->codecpar->channel_layout = AV_CH_LAYOUT_QUAD; break; case 5: fStream->codecpar->channel_layout = AV_CH_LAYOUT_5POINT0; break; case 6: fStream->codecpar->channel_layout = AV_CH_LAYOUT_5POINT1; break; case 8: fStream->codecpar->channel_layout = AV_CH_LAYOUT_7POINT1; break; case 10: fStream->codecpar->channel_layout = AV_CH_LAYOUT_7POINT1_WIDE; break; } } else { // The bits match 1:1 for media_multi_channels and FFmpeg defines. fStream->codecpar->channel_layout = format->u.raw_audio.channel_mask; } #endif } TRACE(" stream->time_base: (%d/%d)\n", fStream->time_base.num, fStream->time_base.den); #if 0 // Write the AVCodecContext pointer to the user data section of the // media_format. For some encoders, it seems to be necessary to use // the AVCodecContext of the AVStream in order to successfully encode // anything and write valid media files. For example some codecs need // to store meta data or global data in the container. app_info appInfo; if (be_app->GetAppInfo(&appInfo) == B_OK) { uchar* userData = format->user_data; *(uint32*)userData = 'ffmp'; userData += sizeof(uint32); *(team_id*)userData = appInfo.team; userData += sizeof(team_id); *(AVCodecContext**)userData = fStream->codec; } #endif return B_OK; } status_t AVFormatWriter::StreamCookie::WriteChunk(const void* chunkBuffer, size_t chunkSize, media_encode_info* encodeInfo) { TRACE_PACKET("AVFormatWriter::StreamCookie[%d]::WriteChunk(%p, %ld, " "start_time: %" B_PRIdBIGTIME ")\n", fStream->index, chunkBuffer, chunkSize, encodeInfo->start_time); BAutolock _(fStreamLock); fPacket->data = const_cast((const uint8_t*)chunkBuffer); fPacket->size = chunkSize; fPacket->stream_index = fStream->index; fPacket->pts = int64_t((double)encodeInfo->start_time * fStream->time_base.den / (1000000.0 * fStream->time_base.num) + 0.5); fPacket->dts = fPacket->pts; fPacket->flags = 0; if ((encodeInfo->flags & B_MEDIA_KEY_FRAME) != 0) fPacket->flags |= AV_PKT_FLAG_KEY; TRACE_PACKET(" PTS: %" PRId64 " (stream->time_base: (%d/%d)\n", fPacket->pts, fStream->time_base.num, fStream->time_base.den); #if 0 // TODO: Eventually, we need to write interleaved packets, but // maybe we are only supposed to use this if we have actually // more than one stream. For the moment, this crashes in AVPacket // shuffling inside libavformat. Maybe if we want to use this, we // need to allocate a separate AVPacket and copy the chunk buffer. int result = av_interleaved_write_frame(fFormatContext, fPacket); if (result < 0) TRACE(" av_interleaved_write_frame(): %d\n", result); #else int result = av_write_frame(fFormatContext, fPacket); if (result < 0) TRACE(" av_write_frame(): %d\n", result); #endif return result == 0 ? B_OK : B_ERROR; } status_t AVFormatWriter::StreamCookie::AddTrackInfo(uint32 code, const void* data, size_t size, uint32 flags) { TRACE("AVFormatWriter::StreamCookie::AddTrackInfo(%" B_PRIu32 ", %p, %ld, %" B_PRIu32 ")\n", code, data, size, flags); BAutolock _(fStreamLock); return B_NOT_SUPPORTED; } // #pragma mark - AVFormatWriter AVFormatWriter::AVFormatWriter() : fFormatContext(avformat_alloc_context()), fCodecOpened(false), fHeaderError(-1), fIOContext(NULL), fStreamLock("stream lock") { TRACE("AVFormatWriter::AVFormatWriter\n"); } AVFormatWriter::~AVFormatWriter() { TRACE("AVFormatWriter::~AVFormatWriter\n"); // Free the streams and close the AVCodecContexts for (unsigned i = 0; i < fFormatContext->nb_streams; i++) { av_freep(&fFormatContext->streams[i]->codecpar); av_freep(&fFormatContext->streams[i]); } avformat_free_context(fFormatContext); av_free(fIOContext->buffer); av_free(fIOContext); } // #pragma mark - status_t AVFormatWriter::Init(const media_file_format* fileFormat) { TRACE("AVFormatWriter::Init()\n"); if (fIOContext == NULL) { uint8* buffer = static_cast(av_malloc(kIOBufferSize)); if (buffer == NULL) return B_NO_MEMORY; // Allocate I/O context and initialize it with buffer // and hook functions, pass ourself as cookie. fIOContext = avio_alloc_context(buffer, kIOBufferSize, 1, this, 0, _Write, _Seek); if (fIOContext == NULL) { av_free(buffer); TRACE("av_alloc_put_byte() failed!\n"); return B_ERROR; } // Setup I/O hooks. This seems to be enough. fFormatContext->pb = fIOContext; } // Set the AVOutputFormat according to fileFormat... fFormatContext->oformat = av_guess_format(fileFormat->short_name, fileFormat->file_extension, fileFormat->mime_type); if (fFormatContext->oformat == NULL) { TRACE(" failed to find AVOuputFormat for %s\n", fileFormat->short_name); return B_NOT_SUPPORTED; } TRACE(" found AVOuputFormat for %s: %s\n", fileFormat->short_name, fFormatContext->oformat->name); return B_OK; } status_t AVFormatWriter::SetCopyright(const char* copyright) { TRACE("AVFormatWriter::SetCopyright(%s)\n", copyright); return B_NOT_SUPPORTED; } status_t AVFormatWriter::CommitHeader() { TRACE("AVFormatWriter::CommitHeader\n"); if (fFormatContext == NULL) return B_NO_INIT; if (fCodecOpened) return B_NOT_ALLOWED; // We need to close the codecs we opened, even in case of failure. fCodecOpened = true; fHeaderError = avformat_write_header(fFormatContext, NULL); #ifdef TRACE_AVFORMAT_WRITER if (fHeaderError < 0) { char errorBuffer[AV_ERROR_MAX_STRING_SIZE]; av_strerror(fHeaderError, errorBuffer, sizeof(errorBuffer)); TRACE(" avformat_write_header(): %s\n", errorBuffer); } else { TRACE(" wrote header\n"); } for (unsigned i = 0; i < fFormatContext->nb_streams; i++) { AVStream* stream = fFormatContext->streams[i]; TRACE(" stream[%u] time_base: (%d/%d)\n", i, stream->time_base.num, stream->time_base.den); } #endif // TRACE_AVFORMAT_WRITER return fHeaderError == 0 ? B_OK : B_ERROR; } status_t AVFormatWriter::Flush() { TRACE("AVFormatWriter::Flush\n"); return B_NOT_SUPPORTED; } status_t AVFormatWriter::Close() { TRACE("AVFormatWriter::Close\n"); if (fFormatContext == NULL) return B_NO_INIT; if (!fCodecOpened) return B_NOT_ALLOWED; // From ffmpeg documentation: [av_write_trailer] may only be called // after a successful call to avformat_write_header. if (fHeaderError != 0) return B_ERROR; int result = av_write_trailer(fFormatContext); if (result < 0) TRACE(" av_write_trailer(): %d\n", result); return result == 0 ? B_OK : B_ERROR; } status_t AVFormatWriter::AllocateCookie(void** _cookie, media_format* format, const media_codec_info* codecInfo) { TRACE("AVFormatWriter::AllocateCookie()\n"); if (fCodecOpened) return B_NOT_ALLOWED; BAutolock _(fStreamLock); if (_cookie == NULL) return B_BAD_VALUE; StreamCookie* cookie = new(std::nothrow) StreamCookie(fFormatContext, &fStreamLock); status_t ret = cookie->Init(format, codecInfo); if (ret != B_OK) { delete cookie; return ret; } *_cookie = cookie; return B_OK; } status_t AVFormatWriter::FreeCookie(void* _cookie) { BAutolock _(fStreamLock); StreamCookie* cookie = reinterpret_cast(_cookie); delete cookie; return B_OK; } // #pragma mark - status_t AVFormatWriter::SetCopyright(void* cookie, const char* copyright) { TRACE("AVFormatWriter::SetCopyright(%p, %s)\n", cookie, copyright); return B_NOT_SUPPORTED; } status_t AVFormatWriter::AddTrackInfo(void* _cookie, uint32 code, const void* data, size_t size, uint32 flags) { TRACE("AVFormatWriter::AddTrackInfo(%" B_PRIu32 ", %p, %ld, %" B_PRIu32 ")\n", code, data, size, flags); if (fHeaderError != 0) return B_ERROR; StreamCookie* cookie = reinterpret_cast(_cookie); return cookie->AddTrackInfo(code, data, size, flags); } status_t AVFormatWriter::WriteChunk(void* _cookie, const void* chunkBuffer, size_t chunkSize, media_encode_info* encodeInfo) { TRACE_PACKET("AVFormatWriter::WriteChunk(%p, %ld, %p)\n", chunkBuffer, chunkSize, encodeInfo); if (fHeaderError != 0) return B_ERROR; StreamCookie* cookie = reinterpret_cast(_cookie); return cookie->WriteChunk(chunkBuffer, chunkSize, encodeInfo); } // #pragma mark - I/O hooks /*static*/ int AVFormatWriter::_Write(void* cookie, uint8* buffer, int bufferSize) { TRACE_IO("AVFormatWriter::_Write(%p, %p, %d)\n", cookie, buffer, bufferSize); AVFormatWriter* writer = reinterpret_cast(cookie); ssize_t written = writer->fTarget->Write(buffer, bufferSize); TRACE_IO(" written: %ld\n", written); return (int)written; } /*static*/ off_t AVFormatWriter::_Seek(void* cookie, off_t offset, int whence) { TRACE_IO("AVFormatWriter::_Seek(%p, %lld, %d)\n", cookie, offset, whence); AVFormatWriter* writer = reinterpret_cast(cookie); BMediaIO* mediaIO = dynamic_cast(writer->fTarget); if (mediaIO == NULL) return -1; // Support for special file size retrieval API without seeking anywhere: if (whence == AVSEEK_SIZE) { off_t size; if (mediaIO->GetSize(&size) == B_OK) return size; return -1; } off_t position = mediaIO->Seek(offset, whence); TRACE_IO(" position: %lld\n", position); if (position < 0) return -1; return position; }