diff --git a/src/media/audio/audio_rtp_session.cpp b/src/media/audio/audio_rtp_session.cpp index edb7d7b4b74dd30649999c8a6eb28c71342b71bf..eb1f779a89610cad0b03479b66343bfa04eb8fc0 100644 --- a/src/media/audio/audio_rtp_session.cpp +++ b/src/media/audio/audio_rtp_session.cpp @@ -118,9 +118,9 @@ AudioSender::setup(SocketPair& socketPair) try { /* Encoder setup */ - RING_DBG("audioEncoder_->openOutput %s", dest_.c_str()); + RING_DBG("audioEncoder_->openLiveOutput %s", dest_.c_str()); audioEncoder_->setMuted(muteState_); - audioEncoder_->openOutput(dest_, args_); + audioEncoder_->openLiveOutput(dest_, args_); audioEncoder_->setInitSeqVal(seqVal_); audioEncoder_->setIOContext(muxContext_); audioEncoder_->startIO(); diff --git a/src/media/media_encoder.cpp b/src/media/media_encoder.cpp index 7a0b5fe51334c5c8b9f52fbb89e462430cf14f85..7207c862d49bcacc8aafb969786c1de857eb5187 100644 --- a/src/media/media_encoder.cpp +++ b/src/media/media_encoder.cpp @@ -29,6 +29,10 @@ #include "string_utils.h" #include "logger.h" +extern "C" { +#include <libavutil/parseutils.h> +} + #include <iostream> #include <sstream> #include <algorithm> @@ -49,8 +53,9 @@ MediaEncoder::~MediaEncoder() if (outputCtx_->priv_data) av_write_trailer(outputCtx_); - if (encoderCtx_) - avcodec_close(encoderCtx_); + for (auto encoderCtx : encoders_) + if (encoderCtx) + avcodec_close(encoderCtx); avformat_free_context(outputCtx_); } @@ -119,12 +124,12 @@ MediaEncoder::getLastSeqValue() std::string MediaEncoder::getEncoderName() const { - return encoderCtx_->codec->name; + return encoders_[currentStreamIdx_]->codec->name; } void -MediaEncoder::openOutput(const std::string& filename, - const ring::MediaDescription& args) +MediaEncoder::openLiveOutput(const std::string& filename, + const ring::MediaDescription& args) { setOptions(args); AVOutputFormat *oformat = av_guess_format("rtp", filename.c_str(), nullptr); @@ -136,7 +141,7 @@ MediaEncoder::openOutput(const std::string& filename, outputCtx_->oformat = oformat; #if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 7, 100) - // c_str guarantees NULL termination + // c_str guarantees null termination outputCtx_->url = av_strdup(filename.c_str()); // must be compatible with av_free #else strncpy(outputCtx_->filename, filename.c_str(), sizeof(outputCtx_->filename)); @@ -144,98 +149,129 @@ MediaEncoder::openOutput(const std::string& filename, outputCtx_->filename[sizeof(outputCtx_->filename) - 1] = '\0'; #endif + addStream(args.codec->systemCodecInfo, args.parameters); +} + +void +MediaEncoder::openFileOutput(const std::string& filename, std::map<std::string, std::string> options) +{ + avformat_free_context(outputCtx_); + avformat_alloc_output_context2(&outputCtx_, nullptr, nullptr, filename.c_str()); + auto bitrate = SystemCodecInfo::DEFAULT_MAX_BITRATE; + auto quality = SystemCodecInfo::DEFAULT_CODEC_QUALITY; + // ensure all options retrieved later on are in options_ (insert does nothing if key exists) + options.insert({"max_rate", ring::to_string(bitrate)}); + options.insert({"crf", ring::to_string(quality)}); + options.insert({"sample_rate", "8000"}); + options.insert({"channels", "2"}); + int sampleRate = atoi(options["sample_rate"].c_str()); + options.insert({"frame_size", ring::to_string(static_cast<unsigned>(0.02*sampleRate))}); + options.insert({"width", "320"}); + options.insert({"height", "240"}); + options.insert({"framerate", "30"}); + for (const auto& it : options) + av_dict_set(&options_, it.first.c_str(), it.second.c_str(), 0); + // for a file output, addStream is done by the caller, as there may be multiple streams +} + +int +MediaEncoder::addStream(const SystemCodecInfo& systemCodecInfo, std::string parameters) +{ + AVCodec* outputCodec = nullptr; + AVCodecContext* encoderCtx = nullptr; /* find the video encoder */ - if (args.codec->systemCodecInfo.avcodecId == AV_CODEC_ID_H263) + if (systemCodecInfo.avcodecId == AV_CODEC_ID_H263) // For H263 encoding, we force the use of AV_CODEC_ID_H263P (H263-1998) // H263-1998 can manage all frame sizes while H263 don't // AV_CODEC_ID_H263 decoder will be used for decoding - outputEncoder_ = avcodec_find_encoder(AV_CODEC_ID_H263P); + outputCodec = avcodec_find_encoder(AV_CODEC_ID_H263P); else - outputEncoder_ = avcodec_find_encoder((AVCodecID)args.codec->systemCodecInfo.avcodecId); - if (!outputEncoder_) { - RING_ERR("Encoder \"%s\" not found!", args.codec->systemCodecInfo.name.c_str()); + outputCodec = avcodec_find_encoder(static_cast<AVCodecID>(systemCodecInfo.avcodecId)); + if (!outputCodec) { + RING_ERR("Encoder \"%s\" not found!", systemCodecInfo.name.c_str()); throw MediaEncoderException("No output encoder"); } - prepareEncoderContext(args.codec->systemCodecInfo.mediaType == MEDIA_VIDEO); - auto maxBitrate = 1000 * atoi(av_dict_get(options_, "max_rate", NULL, 0)->value); + encoderCtx = prepareEncoderContext(outputCodec, systemCodecInfo.mediaType == MEDIA_VIDEO); + encoders_.push_back(encoderCtx); + auto maxBitrate = 1000 * atoi(av_dict_get(options_, "max_rate", nullptr, 0)->value); auto bufSize = 2 * maxBitrate; // as recommended (TODO: make it customizable) - auto crf = atoi(av_dict_get(options_, "crf", NULL, 0)->value); + auto crf = atoi(av_dict_get(options_, "crf", nullptr, 0)->value); /* let x264 preset override our encoder settings */ - if (args.codec->systemCodecInfo.avcodecId == AV_CODEC_ID_H264) { - extractProfileLevelID(args.parameters, encoderCtx_); - forcePresetX264(); + if (systemCodecInfo.avcodecId == AV_CODEC_ID_H264) { + extractProfileLevelID(parameters, encoderCtx); + forcePresetX264(encoderCtx); // For H264 : // Streaming => VBV (constrained encoding) + CRF (Constant Rate Factor) if (crf == SystemCodecInfo::DEFAULT_NO_QUALITY) crf = 30; // good value for H264-720p@30 RING_DBG("H264 encoder setup: crf=%u, maxrate=%u, bufsize=%u", crf, maxBitrate, bufSize); - av_opt_set(encoderCtx_->priv_data, "crf", av_dict_get(options_, "crf", NULL, 0)->value, 0); - encoderCtx_->rc_buffer_size = bufSize; - encoderCtx_->rc_max_rate = maxBitrate; - } else if (args.codec->systemCodecInfo.avcodecId == AV_CODEC_ID_VP8) { + av_opt_set(encoderCtx->priv_data, "crf", av_dict_get(options_, "crf", nullptr, 0)->value, 0); + encoderCtx->rc_buffer_size = bufSize; + encoderCtx->rc_max_rate = maxBitrate; + } else if (systemCodecInfo.avcodecId == AV_CODEC_ID_VP8) { // For VP8 : // 1- if quality is set use it // bitrate need to be set. The target bitrate becomes the maximum allowed bitrate // 2- otherwise set rc_max_rate and rc_buffer_size // Using information given on this page: // http://www.webmproject.org/docs/encoder-parameters/ - av_opt_set(encoderCtx_->priv_data, "quality", "realtime", 0); - av_opt_set_int(encoderCtx_->priv_data, "error-resilient", 1, 0); - av_opt_set_int(encoderCtx_->priv_data, "cpu-used", 7, 0); // value obtained from testing - av_opt_set_int(encoderCtx_->priv_data, "lag-in-frames", 0, 0); + av_opt_set(encoderCtx->priv_data, "quality", "realtime", 0); + av_opt_set_int(encoderCtx->priv_data, "error-resilient", 1, 0); + av_opt_set_int(encoderCtx->priv_data, "cpu-used", 7, 0); // value obtained from testing + av_opt_set_int(encoderCtx->priv_data, "lag-in-frames", 0, 0); // allow encoder to drop frames if buffers are full and // to undershoot target bitrate to lessen strain on resources - av_opt_set_int(encoderCtx_->priv_data, "drop-frame", 25, 0); - av_opt_set_int(encoderCtx_->priv_data, "undershoot-pct", 95, 0); - // don't set encoderCtx_->gop_size: let libvpx decide when to insert a keyframe - encoderCtx_->slices = 2; // VP8E_SET_TOKEN_PARTITIONS - encoderCtx_->qmin = 4; - encoderCtx_->qmax = 56; - encoderCtx_->rc_buffer_size = maxBitrate; - encoderCtx_->bit_rate = maxBitrate; + av_opt_set_int(encoderCtx->priv_data, "drop-frame", 25, 0); + av_opt_set_int(encoderCtx->priv_data, "undershoot-pct", 95, 0); + // don't set encoderCtx->gop_size: let libvpx decide when to insert a keyframe + encoderCtx->slices = 2; // VP8E_SET_TOKEN_PARTITIONS + encoderCtx->qmin = 4; + encoderCtx->qmax = 56; + encoderCtx->rc_buffer_size = maxBitrate; + encoderCtx->bit_rate = maxBitrate; if (crf != SystemCodecInfo::DEFAULT_NO_QUALITY) { - av_opt_set(encoderCtx_->priv_data, "crf", av_dict_get(options_, "crf", NULL, 0)->value, 0); + av_opt_set(encoderCtx->priv_data, "crf", av_dict_get(options_, "crf", nullptr, 0)->value, 0); RING_DBG("Using quality factor %d", crf); } else { RING_DBG("Using Max bitrate %d", maxBitrate); } - } else if (args.codec->systemCodecInfo.avcodecId == AV_CODEC_ID_MPEG4) { + } else if (systemCodecInfo.avcodecId == AV_CODEC_ID_MPEG4) { // For MPEG4 : // No CRF avaiable. // Use CBR (set bitrate) - encoderCtx_->rc_buffer_size = maxBitrate; - encoderCtx_->bit_rate = encoderCtx_->rc_min_rate = encoderCtx_->rc_max_rate = maxBitrate; + encoderCtx->rc_buffer_size = maxBitrate; + encoderCtx->bit_rate = encoderCtx->rc_min_rate = encoderCtx->rc_max_rate = maxBitrate; RING_DBG("Using Max bitrate %d", maxBitrate); - } else if (args.codec->systemCodecInfo.avcodecId == AV_CODEC_ID_H263) { - encoderCtx_->bit_rate = encoderCtx_->rc_max_rate = maxBitrate; - encoderCtx_->rc_buffer_size = maxBitrate; + } else if (systemCodecInfo.avcodecId == AV_CODEC_ID_H263) { + encoderCtx->bit_rate = encoderCtx->rc_max_rate = maxBitrate; + encoderCtx->rc_buffer_size = maxBitrate; RING_DBG("Using Max bitrate %d", maxBitrate); } - int ret; - ret = avcodec_open2(encoderCtx_, outputEncoder_, NULL); - if (ret) + if (avcodec_open2(encoderCtx, outputCodec, nullptr) < 0) throw MediaEncoderException("Could not open encoder"); // add video stream to outputformat context - stream_ = avformat_new_stream(outputCtx_, 0); - if (!stream_) + AVStream* stream = avformat_new_stream(outputCtx_, outputCodec); + if (!stream) throw MediaEncoderException("Could not allocate stream"); + currentStreamIdx_ = stream->index; + #ifndef _WIN32 - avcodec_parameters_from_context(stream_->codecpar, encoderCtx_); + avcodec_parameters_from_context(stream->codecpar, encoderCtx); #else - stream_->codec = encoderCtx_; + stream->codec = encoderCtx; #endif #ifdef RING_VIDEO - if (args.codec->systemCodecInfo.mediaType == MEDIA_VIDEO) { + if (systemCodecInfo.mediaType == MEDIA_VIDEO) { // allocate buffers for both scaled (pre-encoder) and encoded frames - const int width = encoderCtx_->width; - const int height = encoderCtx_->height; - const int format = libav_utils::ring_pixel_format((int)encoderCtx_->pix_fmt); + const int width = encoderCtx->width; + const int height = encoderCtx->height; + const int format = libav_utils::ring_pixel_format((int)encoderCtx->pix_fmt); scaledFrameBufferSize_ = videoFrameSize(format, width, height); if (scaledFrameBufferSize_ <= AV_INPUT_BUFFER_MIN_SIZE) throw MediaEncoderException("buffer too small"); @@ -244,6 +280,8 @@ MediaEncoder::openOutput(const std::string& filename, scaledFrame_.setFromMemory(scaledFrameBuffer_.data(), format, width, height); } #endif // RING_VIDEO + + return stream->index; } void MediaEncoder::setInterruptCallback(int (*cb)(void*), void *opaque) @@ -258,14 +296,29 @@ void MediaEncoder::setInterruptCallback(int (*cb)(void*), void *opaque) void MediaEncoder::setIOContext(const std::unique_ptr<MediaIOHandle> &ioctx) { - outputCtx_->pb = ioctx->getContext(); - outputCtx_->packet_size = outputCtx_->pb->buffer_size; + if (ioctx) { + outputCtx_->pb = ioctx->getContext(); + outputCtx_->packet_size = outputCtx_->pb->buffer_size; + } else { + int ret = 0; +#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 7, 100) + const char* filename = outputCtx_->url; +#else + const char* filename = outputCtx_->filename; +#endif + if (!(outputCtx_->oformat->flags & AVFMT_NOFILE)) { + if ((ret = avio_open(&outputCtx_->pb, filename, AVIO_FLAG_WRITE)) < 0) { + throw MediaEncoderException( + std::string("Could not set IO context" + libav_utils::getError(ret)).c_str()); + } + } + } } void MediaEncoder::startIO() { - if (avformat_write_header(outputCtx_, options_ ? &options_ : NULL)) { + if (avformat_write_header(outputCtx_, options_ ? &options_ : nullptr)) { RING_ERR("Could not write header for output file... check codec parameters"); throw MediaEncoderException("Failed to write output file header"); } @@ -300,15 +353,13 @@ MediaEncoder::encode(VideoFrame& input, bool is_keyframe, frame->key_frame = 0; } - int ret = encode(frame, stream_->index); - - return ret; + return encode(frame, currentStreamIdx_); } #endif // RING_VIDEO int MediaEncoder::encode_audio(const AudioBuffer &buffer) { - const int needed_bytes = av_samples_get_buffer_size(NULL, buffer.channels(), + const int needed_bytes = av_samples_get_buffer_size(nullptr, buffer.channels(), buffer.frames(), AV_SAMPLE_FMT_S16, 0); if (needed_bytes < 0) { @@ -322,6 +373,8 @@ int MediaEncoder::encode_audio(const AudioBuffer &buffer) AudioSample *offset_ptr = sample_data; int nb_frames = buffer.frames(); + AVCodecContext* encoderCtx = encoders_[currentStreamIdx_]; + if (not is_muted) { //only fill buffer with samples if not muted buffer.interleave(sample_data); @@ -337,9 +390,9 @@ int MediaEncoder::encode_audio(const AudioBuffer &buffer) if (!frame) return -1; - if (encoderCtx_->frame_size) + if (encoderCtx->frame_size) frame->nb_samples = std::min<int>(nb_frames, - encoderCtx_->frame_size); + encoderCtx->frame_size); else frame->nb_samples = nb_frames; @@ -351,7 +404,7 @@ int MediaEncoder::encode_audio(const AudioBuffer &buffer) sent_samples += frame->nb_samples; const auto buffer_size = \ - av_samples_get_buffer_size(NULL, buffer.channels(), + av_samples_get_buffer_size(nullptr, buffer.channels(), frame->nb_samples, AV_SAMPLE_FMT_S16, 0); int err = avcodec_fill_audio_frame(frame, buffer.channels(), @@ -368,7 +421,7 @@ int MediaEncoder::encode_audio(const AudioBuffer &buffer) nb_frames -= frame->nb_samples; offset_ptr += frame->nb_samples * buffer.channels(); - encode(frame, stream_->index); + encode(frame, currentStreamIdx_); av_frame_free(&frame); } @@ -379,33 +432,33 @@ int MediaEncoder::encode(AVFrame* frame, int streamIdx) { int ret = 0; + AVCodecContext* encoderCtx = encoders_[streamIdx]; AVPacket pkt; av_init_packet(&pkt); - pkt.data = NULL; // packet data will be allocated by the encoder + pkt.data = nullptr; // packet data will be allocated by the encoder pkt.size = 0; - pkt.stream_index = streamIdx; - ret = avcodec_send_frame(encoderCtx_, frame); + ret = avcodec_send_frame(encoderCtx, frame); if (ret < 0) return -1; while (ret >= 0) { - ret = avcodec_receive_packet(encoderCtx_, &pkt); + ret = avcodec_receive_packet(encoderCtx, &pkt); if (ret == AVERROR(EAGAIN)) break; - if (ret < 0) { + if (ret < 0 && ret != AVERROR_EOF) { // we still want to write our frame on EOF RING_ERR() << "Failed to encode frame: " << libav_utils::getError(ret); return ret; } if (pkt.size) { + pkt.stream_index = streamIdx; if (pkt.pts != AV_NOPTS_VALUE) - pkt.pts = av_rescale_q(pkt.pts, encoderCtx_->time_base, - stream_->time_base); + pkt.pts = av_rescale_q(pkt.pts, encoderCtx->time_base, + outputCtx_->streams[streamIdx]->time_base); if (pkt.dts != AV_NOPTS_VALUE) - pkt.dts = av_rescale_q(pkt.dts, encoderCtx_->time_base, - stream_->time_base); - + pkt.dts = av_rescale_q(pkt.dts, encoderCtx->time_base, + outputCtx_->streams[streamIdx]->time_base); // write the compressed frame ret = av_write_frame(outputCtx_, &pkt); @@ -423,7 +476,14 @@ MediaEncoder::encode(AVFrame* frame, int streamIdx) int MediaEncoder::flush() { - return encode(nullptr, stream_->index); + int ret = 0; + for (size_t i = 0; i < outputCtx_->nb_streams; ++i) { + if (encode(nullptr, i) < 0) { + RING_ERR() << "Could not flush stream #" << i; + ret |= 1u << i; // provide a way for caller to know which streams failed + } + } + return -ret; } std::string @@ -431,9 +491,9 @@ MediaEncoder::print_sdp() { /* theora sdp can be huge */ #ifndef _WIN32 - const auto sdp_size = outputCtx_->streams[0]->codecpar->extradata_size + 2048; + const auto sdp_size = outputCtx_->streams[currentStreamIdx_]->codecpar->extradata_size + 2048; #else - const auto sdp_size = outputCtx_->streams[0]->codec->extradata_size + 2048; + const auto sdp_size = outputCtx_->streams[currentStreamIdx_]->codec->extradata_size + 2048; #endif std::string result; std::string sdp(sdp_size, '\0'); @@ -451,84 +511,102 @@ MediaEncoder::print_sdp() return result; } -void MediaEncoder::prepareEncoderContext(bool is_video) +AVCodecContext* MediaEncoder::prepareEncoderContext(AVCodec* outputCodec, bool is_video) { - encoderCtx_ = avcodec_alloc_context3(outputEncoder_); + AVCodecContext* encoderCtx = avcodec_alloc_context3(outputCodec); - auto encoderName = encoderCtx_->av_class->item_name ? - encoderCtx_->av_class->item_name(encoderCtx_) : nullptr; + auto encoderName = encoderCtx->av_class->item_name ? + encoderCtx->av_class->item_name(encoderCtx) : nullptr; if (encoderName == nullptr) encoderName = "encoder?"; - - encoderCtx_->thread_count = std::min(std::thread::hardware_concurrency(), is_video ? 16u : 4u); - RING_DBG("[%s] Using %d threads", encoderName, encoderCtx_->thread_count); - + encoderCtx->thread_count = std::min(std::thread::hardware_concurrency(), is_video ? 16u : 4u); + RING_DBG("[%s] Using %d threads", encoderName, encoderCtx->thread_count); if (is_video) { // resolution must be a multiple of two - encoderCtx_->width = device_.width; - encoderCtx_->height = device_.height; + if (device_.width && device_.height) { + encoderCtx->width = device_.width; + encoderCtx->height = device_.height; + } else { + encoderCtx->width = atoi(av_dict_get(options_, "width", nullptr, 0)->value); + encoderCtx->height = atoi(av_dict_get(options_, "height", nullptr, 0)->value); + } // satisfy ffmpeg: denominator must be 16bit or less value // time base = 1/FPS - av_reduce(&encoderCtx_->time_base.den, &encoderCtx_->time_base.num, - device_.framerate.numerator(), device_.framerate.denominator(), - (1U << 16) - 1); + if (device_.framerate) { + av_dict_set(&options_, "width", ring::to_string(device_.width).c_str(), 0); + av_reduce(&encoderCtx->time_base.den, &encoderCtx->time_base.num, + device_.framerate.numerator(), device_.framerate.denominator(), + (1U << 16) - 1); + } else { + // get from options_, else default to 30 fps + auto v = av_dict_get(options_, "framerate", nullptr, 0); + AVRational framerate = AVRational{30, 1}; + if (v) + av_parse_ratio_quiet(&framerate, v->value, 120); + if (framerate.den == 0) + framerate.den = 1; + av_reduce(&encoderCtx->time_base.den, &encoderCtx->time_base.num, + framerate.num, framerate.den, + (1U << 16) - 1); + } // emit one intra frame every gop_size frames - encoderCtx_->max_b_frames = 0; - encoderCtx_->pix_fmt = AV_PIX_FMT_YUV420P; // TODO: option me ! + encoderCtx->max_b_frames = 0; + encoderCtx->pix_fmt = AV_PIX_FMT_YUV420P; // TODO: option me ! // Fri Jul 22 11:37:59 EDT 2011:tmatth:XXX: DON'T set this, we want our // pps and sps to be sent in-band for RTP // This is to place global headers in extradata instead of every // keyframe. - // encoderCtx_->flags |= CODEC_FLAG_GLOBAL_HEADER; - + // encoderCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; } else { - encoderCtx_->sample_fmt = AV_SAMPLE_FMT_S16; - auto v = av_dict_get(options_, "sample_rate", NULL, 0); + encoderCtx->sample_fmt = AV_SAMPLE_FMT_S16; + auto v = av_dict_get(options_, "sample_rate", nullptr, 0); if (v) { - encoderCtx_->sample_rate = atoi(v->value); - encoderCtx_->time_base = AVRational{1, encoderCtx_->sample_rate}; + encoderCtx->sample_rate = atoi(v->value); + encoderCtx->time_base = AVRational{1, encoderCtx->sample_rate}; } else { RING_WARN("[%s] No sample rate set", encoderName); - encoderCtx_->sample_rate = 8000; + encoderCtx->sample_rate = 8000; } - v = av_dict_get(options_, "channels", NULL, 0); + v = av_dict_get(options_, "channels", nullptr, 0); if (v) { auto c = std::atoi(v->value); if (c > 2 or c < 1) { RING_WARN("[%s] Clamping invalid channel value %d", encoderName, c); c = 1; } - encoderCtx_->channels = c; + encoderCtx->channels = c; } else { RING_WARN("[%s] Channels not set", encoderName); - encoderCtx_->channels = 1; + encoderCtx->channels = 1; } - encoderCtx_->channel_layout = encoderCtx_->channels == 2 ? AV_CH_LAYOUT_STEREO : AV_CH_LAYOUT_MONO; + encoderCtx->channel_layout = encoderCtx->channels == 2 ? AV_CH_LAYOUT_STEREO : AV_CH_LAYOUT_MONO; - v = av_dict_get(options_, "frame_size", NULL, 0); + v = av_dict_get(options_, "frame_size", nullptr, 0); if (v) { - encoderCtx_->frame_size = atoi(v->value); - RING_DBG("[%s] Frame size %d", encoderName, encoderCtx_->frame_size); + encoderCtx->frame_size = atoi(v->value); + RING_DBG("[%s] Frame size %d", encoderName, encoderCtx->frame_size); } else { RING_WARN("[%s] Frame size not set", encoderName); } } + + return encoderCtx; } -void MediaEncoder::forcePresetX264() +void MediaEncoder::forcePresetX264(AVCodecContext* encoderCtx) { const char *speedPreset = "ultrafast"; - if (av_opt_set(encoderCtx_->priv_data, "preset", speedPreset, 0)) + if (av_opt_set(encoderCtx->priv_data, "preset", speedPreset, 0)) RING_WARN("Failed to set x264 preset '%s'", speedPreset); const char *tune = "zerolatency"; - if (av_opt_set(encoderCtx_->priv_data, "tune", tune, 0)) + if (av_opt_set(encoderCtx->priv_data, "tune", tune, 0)) RING_WARN("Failed to set x264 tune '%s'", tune); } @@ -592,4 +670,13 @@ MediaEncoder::useCodec(const ring::AccountCodecInfo* codec) const noexcept return codec_.get() == codec; } +unsigned +MediaEncoder::getStreamCount() const +{ + if (outputCtx_) + return outputCtx_->nb_streams; + else + return 0; +} + } // namespace ring diff --git a/src/media/media_encoder.h b/src/media/media_encoder.h index 44e824411844bea3c630ffb262b5819c48bffab8..fd95af5e36c3fac1b4c14c203197b085d1f7ad36 100644 --- a/src/media/media_encoder.h +++ b/src/media/media_encoder.h @@ -30,6 +30,7 @@ #include "noncopyable.h" #include "media_buffer.h" +#include "media_codec.h" #include "media_device.h" #include <map> @@ -63,7 +64,9 @@ public: void setInterruptCallback(int (*cb)(void*), void *opaque); void setDeviceOptions(const DeviceParams& args); - void openOutput(const std::string& filename, const MediaDescription& args); + void openLiveOutput(const std::string& filename, const MediaDescription& args); + void openFileOutput(const std::string& filename, std::map<std::string, std::string> options); + int addStream(const SystemCodecInfo& codec, std::string parameters = ""); void startIO(); void setIOContext(const std::unique_ptr<MediaIOHandle> &ioctx); @@ -80,7 +83,7 @@ public: std::string print_sdp(); /* getWidth and getHeight return size of the encoded frame. - * Values have meaning only after openOutput call. + * Values have meaning only after openLiveOutput call. */ int getWidth() const { return device_.width; } int getHeight() const { return device_.height; } @@ -92,18 +95,19 @@ public: bool useCodec(const AccountCodecInfo* codec) const noexcept; + unsigned getStreamCount() const; + private: NON_COPYABLE(MediaEncoder); void setOptions(const MediaDescription& args); void setScaleDest(void *data, int width, int height, int pix_fmt); - void prepareEncoderContext(bool is_video); - void forcePresetX264(); + AVCodecContext* prepareEncoderContext(AVCodec* outputCodec, bool is_video); + void forcePresetX264(AVCodecContext* encoderCtx); void extractProfileLevelID(const std::string ¶meters, AVCodecContext *ctx); - AVCodec *outputEncoder_ = nullptr; - AVCodecContext *encoderCtx_ = nullptr; + std::vector<AVCodecContext*> encoders_; AVFormatContext *outputCtx_ = nullptr; - AVStream *stream_ = nullptr; + int currentStreamIdx_ = -1; unsigned sent_samples = 0; #ifdef RING_VIDEO @@ -113,7 +117,6 @@ private: std::vector<uint8_t> scaledFrameBuffer_; int scaledFrameBufferSize_ = 0; - int streamIndex_ = -1; bool is_muted = false; protected: diff --git a/src/media/video/video_sender.cpp b/src/media/video/video_sender.cpp index a0c6ba2c29d7037396c1d992da296c13d62ad3de..8fb9916e0e68b682bcc65aa8adf90727f99faa55 100644 --- a/src/media/video/video_sender.cpp +++ b/src/media/video/video_sender.cpp @@ -44,7 +44,7 @@ VideoSender::VideoSender(const std::string& dest, const DeviceParams& dev, { videoEncoder_->setDeviceOptions(dev); keyFrameFreq_ = dev.framerate.numerator() * KEY_FRAME_PERIOD; - videoEncoder_->openOutput(dest, args); + videoEncoder_->openLiveOutput(dest, args); videoEncoder_->setInitSeqVal(seqVal); videoEncoder_->setIOContext(muxContext_); videoEncoder_->startIO(); diff --git a/test/unitTest/Makefile.am b/test/unitTest/Makefile.am index 63606c3c63d4d7bc1b9455a538991880f2a43cd8..7e71fa76781e871a1f8fb4db3b5a79fa32478f93 100644 --- a/test/unitTest/Makefile.am +++ b/test/unitTest/Makefile.am @@ -61,4 +61,10 @@ ut_string_utils_SOURCES = string_utils/testString_utils.cpp check_PROGRAMS += ut_video_input ut_video_input_SOURCES = media/video/testVideo_input.cpp +# +# media_encoder +# +check_PROGRAMS += ut_media_encoder +ut_media_encoder_SOURCES = media/test_media_encoder.cpp + TESTS = $(check_PROGRAMS) diff --git a/test/unitTest/media/test_media_encoder.cpp b/test/unitTest/media/test_media_encoder.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2a1fb24eb539355bf074dfb1336fee91fbd2320a --- /dev/null +++ b/test/unitTest/media/test_media_encoder.cpp @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2018 Savoir-faire Linux Inc. + * + * Author: Philippe Gorley <philippe.gorley@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <cppunit/TestAssert.h> +#include <cppunit/TestFixture.h> +#include <cppunit/extensions/HelperMacros.h> + +#include "dring.h" +#include "fileutils.h" +#include "libav_deps.h" +#include "media_encoder.h" +#include "media_io_handle.h" +#include "system_codec_container.h" + +#include "../../test_runner.h" + +namespace ring { namespace test { + +class MediaEncoderTest : public CppUnit::TestFixture { +public: + static std::string name() { return "media_encoder"; } + + void setUp(); + void tearDown(); + +private: + void testMultiStream(); + + CPPUNIT_TEST_SUITE(MediaEncoderTest); + CPPUNIT_TEST(testMultiStream); + CPPUNIT_TEST_SUITE_END(); + + std::unique_ptr<MediaEncoder> encoder_; + std::unique_ptr<MediaIOHandle> ioHandle_; + std::vector<std::string> files_; + std::string cacheDir_; +}; + +CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(MediaEncoderTest, MediaEncoderTest::name()); + +void +MediaEncoderTest::setUp() +{ + DRing::init(DRing::InitFlag(DRing::DRING_FLAG_DEBUG | DRing::DRING_FLAG_CONSOLE_LOG)); + libav_utils::ring_avcodec_init(); + encoder_.reset(new MediaEncoder); + cacheDir_ = fileutils::get_cache_dir() + DIR_SEPARATOR_CH; + files_.push_back(cacheDir_ + "test.mkv"); +} + +void +MediaEncoderTest::tearDown() +{ + // clean up behind ourselves + for (const auto& file : files_) + fileutils::remove(cacheDir_ + file); + DRing::fini(); +} + +static AVFrame* +getVideoFrame(int width, int height, int frame_index) +{ + int x, y; + AVFrame* frame = av_frame_alloc(); + if (!frame) + return nullptr; + + frame->format = AV_PIX_FMT_YUV420P; + frame->width = width; + frame->height = height; + + if (av_frame_get_buffer(frame, 32) < 0) { + av_frame_free(&frame); + return nullptr; + } + + /* Y */ + for (y = 0; y < height; y++) + for (x = 0; x < width; x++) + frame->data[0][y * frame->linesize[0] + x] = x + y + frame_index * 3; + + /* Cb and Cr */ + for (y = 0; y < height / 2; y++) { + for (x = 0; x < width / 2; x++) { + frame->data[1][y * frame->linesize[1] + x] = 128 + y + frame_index * 2; + frame->data[2][y * frame->linesize[2] + x] = 64 + x + frame_index * 5; + } + } + + return frame; +} + +static AVFrame* +getAudioFrame(int sampleRate, int nbSamples, int nbChannels) +{ + const constexpr float pi = 3.14159265358979323846264338327950288; // M_PI + const float tincr = 2 * pi * 440.0 / sampleRate; + float t = 0; + AVFrame* frame = av_frame_alloc(); + if (!frame) + return nullptr; + + frame->format = AV_SAMPLE_FMT_S16; + frame->channels = nbChannels; + frame->channel_layout = av_get_default_channel_layout(nbChannels); + frame->nb_samples = nbSamples; + frame->sample_rate = sampleRate; + + if (av_frame_get_buffer(frame, 0) < 0) { + av_frame_free(&frame); + return nullptr; + } + + auto samples = reinterpret_cast<uint16_t*>(frame->data[0]); + for (int i = 0; i < 200; ++i) { + for (int j = 0; j < nbSamples; ++j) { + samples[2 * j] = static_cast<int>(sin(t) * 10000); + for (int k = 1; k < nbChannels; ++k) { + samples[2 * j + k] = samples[2 * j]; + } + t += tincr; + } + } + + return frame; +} + +void +MediaEncoderTest::testMultiStream() +{ + const constexpr int sampleRate = 48000; + const constexpr int nbChannels = 2; + const constexpr int width = 320; + const constexpr int height = 240; + std::map<std::string, std::string> options; + options["sample_rate"] = std::to_string(sampleRate); + options["channels"] = std::to_string(nbChannels); + options["width"] = std::to_string(width); + options["height"] = std::to_string(height); + auto vp8Codec = std::static_pointer_cast<ring::SystemVideoCodecInfo>( + getSystemCodecContainer()->searchCodecByName("VP8", ring::MEDIA_VIDEO) + ); + auto opusCodec = std::static_pointer_cast<SystemAudioCodecInfo>( + getSystemCodecContainer()->searchCodecByName("opus", ring::MEDIA_AUDIO) + ); + + try { + encoder_->openFileOutput(cacheDir_ + "test.mkv", options); + encoder_->setIOContext(ioHandle_); + int videoIdx = encoder_->addStream(*vp8Codec.get()); + CPPUNIT_ASSERT(videoIdx >= 0); + CPPUNIT_ASSERT(encoder_->getStreamCount() == 1); + int audioIdx = encoder_->addStream(*opusCodec.get()); + CPPUNIT_ASSERT(audioIdx >= 0); + CPPUNIT_ASSERT(videoIdx != audioIdx); + CPPUNIT_ASSERT(encoder_->getStreamCount() == 2); + encoder_->startIO(); + + int sentSamples = 0; + AVFrame* audio = nullptr; + AVFrame* video = nullptr; + for (int i = 0; i < 25; ++i) { + audio = getAudioFrame(sampleRate, 0.02*sampleRate, nbChannels); + CPPUNIT_ASSERT(audio); + audio->pts = sentSamples; + video = getVideoFrame(width, height, i); + CPPUNIT_ASSERT(video); + video->pts = i; + + CPPUNIT_ASSERT(encoder_->encode(audio, audioIdx) >= 0); + sentSamples += audio->nb_samples; + CPPUNIT_ASSERT(encoder_->encode(video, videoIdx) >= 0); + + av_frame_free(&audio); + av_frame_free(&video); + } + CPPUNIT_ASSERT(encoder_->flush() >= 0); + } catch (const MediaEncoderException& e) { + CPPUNIT_FAIL(e.what()); + } +} + +}} // namespace ring::test + +RING_TEST_RUNNER(ring::test::MediaEncoderTest::name());