blob: 7437c1c7f36a27d1a142cc6c48fe8132a5f4a927 [file] [log] [blame]
// Copyright 2018 The Android Open Source Project
//
// This software is licensed under the terms of the GNU General Public
// License version 2, as published by the Free Software Foundation, and
// may be copied, distributed, and modified under those terms.
//
// 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.
//
// Copyright (c) 2003 Fabrice Bellard
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
#include "android/recording/GifConverter.h"
#include "android/base/Log.h"
#include "android/recording/AVScopedPtr.h"
#include "android/utils/debug.h"
extern "C" {
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
}
#include <functional>
#include <memory>
static constexpr int SCALE_FLAGS = SWS_BICUBIC;
namespace android {
namespace recording {
namespace {
class GifConverterImpl {
public:
explicit GifConverterImpl(android::base::StringView inFilename,
android::base::StringView outFilename,
uint32_t bitrate);
bool run();
private:
bool initialize(android::base::StringView inFilename,
android::base::StringView outFilename,
uint32_t bitrate);
bool initInputContext(android::base::StringView inFilename);
bool initOutputContext(android::base::StringView outFilename,
uint32_t bitrate);
bool initConversionContext();
bool getNextVideoPacket(AVPacket* pkt);
int encodeWriteFrame(AVFrame* filt_frame, int* got_frame);
int flushEncoder();
private:
bool mIsValid = false;
AVScopedPtr<AVFormatContext> mInputContext;
AVScopedPtr<AVFormatContext> mOutputContext;
AVScopedPtr<SwsContext> mSwsContext;
AVCodecContext* mInVideoCodecCxt = nullptr;
AVCodecContext* mOutVideoCodecCxt = nullptr;
AVStream* mOutputStream = nullptr;
int mVideoStreamIndex = -1;
};
GifConverterImpl::GifConverterImpl(android::base::StringView inFilename,
android::base::StringView outFilename,
uint32_t bitrate) {
mIsValid = initialize(inFilename, outFilename, bitrate);
}
bool GifConverterImpl::initialize(android::base::StringView inFilename,
android::base::StringView outFilename,
uint32_t bitrate) {
// Initialize libavcodec, and register all codecs and formats. does not hurt
// to register multiple times
av_register_all();
return initInputContext(inFilename) &&
initOutputContext(outFilename, bitrate);
}
bool GifConverterImpl::initInputContext(android::base::StringView inFilename) {
auto inFilenameValidated = android::base::c_str(inFilename);
int ret;
// open the input video file
AVFormatContext* ifmt_ctx = nullptr;
if ((ret = avformat_open_input(&ifmt_ctx, inFilenameValidated, nullptr,
nullptr)) < 0) {
LOG(ERROR) << "Cannot open input file: [" << inFilenameValidated << "]";
return false;
}
mInputContext = makeAVScopedPtr(ifmt_ctx);
// Open the codec for the input file.
if ((ret = avformat_find_stream_info(mInputContext.get(), NULL)) < 0) {
LOG(ERROR) << "Cannot find stream information";
return false;
}
av_dump_format(mInputContext.get(), 0, inFilenameValidated, 0);
// find the video stream, GIF supports only video, no audio
for (int i = 0; i < mInputContext->nb_streams; i++) {
AVStream* stream = mInputContext->streams[i];
AVCodecContext* codec_ctx = stream->codec;
if (codec_ctx->codec_type == AVMEDIA_TYPE_VIDEO) {
codec_ctx->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL;
// Open decoder
ret = avcodec_open2(
codec_ctx, avcodec_find_decoder(codec_ctx->codec_id), NULL);
if (ret < 0) {
LOG(ERROR) << "Failed to open decoder for stream #" << i;
return false;
}
mInVideoCodecCxt = codec_ctx;
mVideoStreamIndex = i;
break;
}
}
if (!mInVideoCodecCxt) {
LOG(ERROR) << "Cannot find video stream";
return false;
}
return true;
}
bool GifConverterImpl::initOutputContext(android::base::StringView outFilename,
uint32_t bitrate) {
auto outFilenameValidated = android::base::c_str(outFilename);
// open the output gif file
AVFormatContext* ofmt_ctx = nullptr;
avformat_alloc_output_context2(&ofmt_ctx, nullptr, nullptr,
outFilenameValidated);
if (ofmt_ctx == nullptr) {
LOG(ERROR) << "Could not create output context for ["
<< outFilenameValidated << "]";
return false;
}
mOutputContext = makeAVScopedPtr(ofmt_ctx);
AVStream* out_stream = avformat_new_stream(mOutputContext.get(), nullptr);
if (out_stream == NULL) {
LOG(ERROR) << "Failed allocating output stream";
return false;
}
mOutputStream = out_stream;
AVCodec* encoder = avcodec_find_encoder(AV_CODEC_ID_GIF);
if (encoder == nullptr) {
LOG(ERROR) << "GIF encoder not found";
return false;
}
AVCodecContext* enc_ctx = mOutputStream->codec;
if ((encoder->capabilities & CODEC_CAP_EXPERIMENTAL) != 0) {
enc_ctx->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL;
}
enc_ctx->height = mInVideoCodecCxt->height;
enc_ctx->width = mInVideoCodecCxt->width;
enc_ctx->bit_rate = bitrate;
enc_ctx->sample_aspect_ratio = mInVideoCodecCxt->sample_aspect_ratio;
// take first format from list of supported formats, which should be
// AV_PIX_FMT_RGB8
enc_ctx->pix_fmt = encoder->pix_fmts[0];
enc_ctx->time_base = mInVideoCodecCxt->time_base;
int ret = avcodec_open2(enc_ctx, encoder, NULL);
if (ret < 0) {
LOG(ERROR) << "Cannot open video encoder for GIF";
return false;
}
mOutVideoCodecCxt = enc_ctx;
if (mOutputContext->oformat->flags & AVFMT_GLOBALHEADER)
enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
av_dump_format(mOutputContext.get(), 0, outFilenameValidated, 1);
if (!(mOutputContext->oformat->flags & AVFMT_NOFILE)) {
ret = avio_open(&mOutputContext->pb, outFilenameValidated,
AVIO_FLAG_WRITE);
if (ret < 0) {
LOG(ERROR) << "Could not open output file [" << outFilenameValidated
<< "]";
return false;
}
}
return true;
}
bool GifConverterImpl::getNextVideoPacket(AVPacket* pkt) {
// Get next video packet
while (av_read_frame(mInputContext.get(), pkt) >= 0) {
if (pkt->stream_index != mVideoStreamIndex) {
// Ignore audio packets
continue;
}
av_packet_rescale_ts(
pkt, mInputContext->streams[mVideoStreamIndex]->time_base,
mInputContext->streams[mVideoStreamIndex]->codec->time_base);
return true;
}
return false;
}
bool GifConverterImpl::run() {
if (!mIsValid) {
LOG(ERROR) << "Gif converter was given invalid parameters";
return false;
}
// init muxer, write output file header
int ret = avformat_write_header(mOutputContext.get(), nullptr);
if (ret < 0) {
LOG(ERROR) << "Error occurred in avformat_write_header()";
return false;
}
SwsContext* sws_ctx = nullptr;
if (mInVideoCodecCxt->pix_fmt != mOutVideoCodecCxt->pix_fmt) {
sws_ctx = sws_getContext(mOutVideoCodecCxt->width, mOutVideoCodecCxt->height,
mInVideoCodecCxt->pix_fmt, mOutVideoCodecCxt->width,
mOutVideoCodecCxt->height,
/*AV_PIX_FMT_RGBA*/ mOutVideoCodecCxt->pix_fmt,
SCALE_FLAGS, nullptr, nullptr, nullptr);
if (sws_ctx == nullptr) {
LOG(ERROR) << "Could not initialize the conversion context";
return false;
}
}
mSwsContext = makeAVScopedPtr(sws_ctx);
// read all packets, decode, convert, then encode to gif format
int64_t last_pts = -1;
int got_frame;
AVPacket packet = {0};
while (getNextVideoPacket(&packet)) {
auto pPacket = makeAVScopedPtr(&packet);
// for GIF, it seems we can't use a shared frame, so we have to alloc
// it here inside the loop
AVFrame* frame = av_frame_alloc();
if (!frame) {
ret = AVERROR(ENOMEM);
break;
}
auto pFrame = makeAVScopedPtr(frame);
ret = avcodec_decode_video2(
mInputContext->streams[mVideoStreamIndex]->codec, pFrame.get(),
&got_frame, &packet);
if (ret < 0) {
LOG(ERROR) << "Decoding failed";
break;
}
ret = 0;
if (got_frame) {
// Rescale pts to codec timebase
pFrame->pts = av_frame_get_best_effort_timestamp(pFrame.get());
// correct invalid pts in the rare cases
// Need to check the scaled value if the output time_base has a
// smaller resolution than the input time_base
if (mOutputContext->streams[mVideoStreamIndex]
->codec->time_base.den >
mOutputContext->streams[mVideoStreamIndex]->time_base.den) {
int64_t last_scaled_pts = -1;
int64_t scaled_pts = av_rescale_q(
pFrame->pts,
mOutputContext->streams[mVideoStreamIndex]
->codec->time_base,
mOutputContext->streams[mVideoStreamIndex]->time_base);
if (last_pts > 0) {
last_scaled_pts = av_rescale_q(
last_pts,
mOutputContext->streams[mVideoStreamIndex]
->codec->time_base,
mOutputContext->streams[mVideoStreamIndex]
->time_base);
}
if (scaled_pts <= last_scaled_pts) {
pFrame->pts = av_rescale_q(
last_scaled_pts + 1,
mOutputContext->streams[mVideoStreamIndex]
->time_base,
mOutputContext->streams[mVideoStreamIndex]
->codec->time_base);
}
} else {
if (pFrame->pts <= last_pts) {
pFrame->pts = last_pts + 1;
}
}
last_pts = pFrame->pts;
// Rescale the video pixel data
AVFrame* tmp_frame = av_frame_alloc();
if (!tmp_frame) {
ret = AVERROR(ENOMEM);
break;
}
auto pTmpFrame = makeAVScopedPtr(tmp_frame);
pTmpFrame->format = mOutVideoCodecCxt->pix_fmt;
pTmpFrame->width = mOutVideoCodecCxt->width;
pTmpFrame->height = mOutVideoCodecCxt->height;
// allocate the buffer for the frame data
ret = av_frame_get_buffer(pTmpFrame.get(), 32);
if (ret < 0) {
LOG(ERROR) << "Could not allocate video frame data.";
break;
}
sws_scale(mSwsContext.get(), pFrame->data, pFrame->linesize, 0,
mOutVideoCodecCxt->height, pTmpFrame->data, pTmpFrame->linesize);
pTmpFrame->pts = pFrame->pts;
ret = encodeWriteFrame(pTmpFrame.get(), nullptr);
if (ret < 0) {
break;
}
}
}
if (ret == AVERROR_EOF) {
ret = 0;
}
if (ret == 0) {
// flush encoder
flushEncoder();
av_write_trailer(mOutputContext.get());
}
// Close the output file and free the output context
mOutputContext.reset();
return ret == 0;
}
int GifConverterImpl::encodeWriteFrame(AVFrame* filt_frame, int* got_frame) {
int ret;
int got_frame_local;
AVPacket enc_pkt;
if (!got_frame)
got_frame = &got_frame_local;
enc_pkt.data = NULL;
enc_pkt.size = 0;
av_init_packet(&enc_pkt);
ret = avcodec_encode_video2(
mOutputContext->streams[mVideoStreamIndex]->codec, &enc_pkt,
filt_frame, got_frame);
if (ret < 0)
return ret;
if (!(*got_frame))
return 0;
// prepare packet for muxing
enc_pkt.stream_index = mVideoStreamIndex;
av_packet_rescale_ts(
&enc_pkt,
mOutputContext->streams[mVideoStreamIndex]->codec->time_base,
mOutputContext->streams[mVideoStreamIndex]->time_base);
// mux encoded frame
// DO NOT free or unref enc_pkt once interleaved.
// av_interleaved_write_frame() will take ownership of the packet once
// passed in.
ret = av_interleaved_write_frame(mOutputContext.get(), &enc_pkt);
return ret;
}
// Gif conversion helper
// static
int GifConverterImpl::flushEncoder() {
int ret;
int got_frame;
if (!(mOutputContext->streams[mVideoStreamIndex]
->codec->codec->capabilities &
AV_CODEC_CAP_DELAY))
return 0;
while (1) {
av_log(NULL, AV_LOG_INFO, "Flushing stream #%u encoder\n",
mVideoStreamIndex);
ret = encodeWriteFrame(nullptr, &got_frame);
if (ret < 0)
break;
if (!got_frame)
return 0;
}
return ret;
}
} // namespace
bool GifConverter::toAnimatedGif(android::base::StringView inFilename,
android::base::StringView outFilename,
uint32_t bitrate) {
GifConverterImpl converter(inFilename, outFilename, bitrate);
return converter.run();
}
} // namespace recording
} // namespace android