12#include <QTemporaryFile>
15#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
16#include <QJniEnvironment>
19#include <QAndroidJniEnvironment>
20#include <QAndroidJniObject>
21using QJniEnvironment = QAndroidJniEnvironment;
22using QJniObject = QAndroidJniObject;
25#include <klocalizedstring.h>
33#include <libavutil/imgutils.h>
34#include <libavutil/pixfmt.h>
35#include <libswscale/swscale.h>
77 QString
key()
const override
122 QVariantMap preferences;
125 preferences.insert(QStringLiteral(
"encoder"), pw->
videoEncoder());
126 preferences.insert(QStringLiteral(
"bitrate"), pw->
videoBitrate());
128 preferences.insert(QStringLiteral(
"aencoder"), pw->
audioEncoder());
129 preferences.insert(QStringLiteral(
"abitrate"), pw->
audioBitrate());
136 QString videoEncoder = preferences.value(QStringLiteral(
"encoder")).toString();
146 int videoBitrate = preferences.value(QStringLiteral(
"bitrate")).toInt();
147 if (videoBitrate <= 0) {
156 QString audioEncoder = preferences.value(QStringLiteral(
"aencoder")).toString();
166 int audioBitrate = preferences.value(QStringLiteral(
"abitrate")).toInt();
167 if (audioBitrate <= 0) {
194 outEncoders.reserve(encoders.size());
230 return QStringLiteral(
"android:mp4:h264");
232 return QStringLiteral(
"android:webm:vp8");
234 return QStringLiteral(
"android:mp4:av1");
243 return QStringLiteral(
"MP4/H.264");
245 return QStringLiteral(
"WEBM/VP8");
247 return QStringLiteral(
"MP4/AV1");
257 return QStringLiteral(
"mp4");
259 return QStringLiteral(
"webm");
297 explicit Context(QString *outErrorMessage =
nullptr)
325 m_encoder.callMethod<
void>(
"cancel",
"()V");
327 if (
m_env->ExceptionCheck()) {
328 warnFile <<
"JNI exception occurred cancelling encoder";
329 m_env->ExceptionDescribe();
330 m_env->ExceptionClear();
339 }
else if (!obj.isValid()) {
352 warnFile <<
"Start video encoder error" << result;
353 setErrorMessage(i18n(
"Unsupported video parameters, try lowering the video FPS or size"));
356 warnFile <<
"Start audio encoder error" << result;
358 i18n(
"Unsupported audio parameters, try to re-encode your audio file with a more common sample format, "
359 "sampling rate and channel count"));
363 warnFile <<
"Muxer track error" << result;
376 if (
m_env->ExceptionCheck()) {
378 m_env->ExceptionDescribe();
379 m_env->ExceptionClear();
401 bool allocateImage(
int outputWidth,
int outputHeight, AVPixelFormat outputFormat)
423 return result >= 100;
450 return QString::fromUtf8(mlt_audio_format_name(
m_format));
472 m_profile = std::make_unique<Mlt::Profile>();
515 int64_t position =
m_frame->get_position();
516 int sampleCount = mlt_audio_calculate_frame_samples(fps,
sampleRate, position);
518 if (sampleCount <= 0 || !sampleData) {
536 m_sampleData =
reinterpret_cast<const unsigned char *
>(sampleData);
550 if (availableSamples <= 0) {
556 int chunkSamples = qMin(availableSamples, remainingSamples);
561 int(ctx.
encoder().callMethod<jint>(
"commitAudio",
"(II)I", jint(chunkSamples), jint(chunkSize)));
562 if (ctx.
checkResult(QStringLiteral(
"commitAudio"), commitResult)) {
586 int finishAudioResult = int(ctx.
encoder().callMethod<jint>(
"finishAudio",
"()I"));
587 if (ctx.
checkResult(QStringLiteral(
"finishAudio"), finishAudioResult)) {
635 for (
int formatId : formatIds) {
650 QTemporaryFile tempFile;
651 QString tempFilePath;
652 if (tempFile.open()) {
653 tempFilePath = tempFile.fileName();
656 warnFile <<
"Failed to open temporary file:" << tempFile.errorString();
665 std::unique_ptr<Audio> audio;
666 if (!
settings().audioFile.isEmpty()) {
673 audio = std::make_unique<Audio>(mlt_audio_s16, format->
audioSampleRate(), 2);
674 if (!audio->open(ctx,
settings())) {
678 switch (audio->pullFrame(ctx)) {
693 QJniObject outputPath = QJniObject::fromString(
settings().outputFile);
694 if (ctx.
checkObject(QStringLiteral(
"outputPath"), outputPath)) {
698 QJniObject tempPath = QJniObject::fromString(tempFilePath);
699 if (ctx.
checkObject(QStringLiteral(
"tempPath"), tempPath)) {
703 QJniObject videoEncoderName =
707 jint audioSampleRate;
708 jint audioChannelCount;
709 QJniObject audioEncoderName;
710 QJniObject audioFormatName;
712 audioSampleRate = jint(audio->sampleRate());
713 audioChannelCount = jint(audio->channelCount());
716 audioFormatName = QJniObject::fromString(audio->formatName());
719 audioSampleRate = jint(0);
720 audioChannelCount = jint(0);
724 "org/krita/android/VideoEncoder",
725 "(IIIFLjava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;III)V",
730 outputPath.object<jstring>(),
731 tempPath.object<jstring>(),
732 videoEncoderName.object<jstring>(),
734 audio ? audioEncoderName.object<jstring>() :
nullptr,
735 audio ? audioFormatName.object<jstring>() :
nullptr,
736 jint(audioSampleRate),
737 jint(audioChannelCount),
750 QJniObject activity = QJniObject::callStaticObjectMethod(
"org/qtproject/qt5/android/QtNative",
752 "()Landroid/app/Activity;");
753 if (ctx.
checkObject(QStringLiteral(
"activity"), activity)) {
758 int(ctx.
encoder().callMethod<jint>(
"start",
"(Landroid/content/Context;)I", activity.object<jobject>()));
759 if (ctx.
checkResult(QStringLiteral(
"start"), startResult)) {
788 AVPixelFormat inputPixelFormat;
801 for (
int i = 0; i < instances; ++i) {
805 return prepareVideoResult;
818 uint8_t *dstBuffers[4] = {
nullptr,
nullptr,
nullptr,
nullptr};
819 int dstLinesizes[4] = {0, 0, 0, 0};
820 AVPixelFormat outputPixelFormat;
823 outputPixelFormat = AV_PIX_FMT_YUV420P;
824 dstBuffers[0] = encoderImage.
bufferY;
825 dstBuffers[1] = encoderImage.
bufferU;
826 dstBuffers[2] = encoderImage.
bufferV;
834 outputPixelFormat = AV_PIX_FMT_NV12;
835 dstBuffers[0] = encoderImage.
bufferY;
836 dstBuffers[1] = encoderImage.
bufferU;
843 outputPixelFormat = AV_PIX_FMT_NV21;
844 dstBuffers[0] = encoderImage.
bufferY;
845 dstBuffers[1] = encoderImage.
bufferV;
851 .arg(quintptr(encoderImage.
bufferU), 0, 16)
853 .arg(quintptr(encoderImage.
bufferV), 0, 16)
870 if (instances == 1) {
872 const uint8_t *srcBuffers[] = {inputImage.bits(),
nullptr,
nullptr,
nullptr};
873 const int srcLinesizes[] = {inputImage.bytesPerLine(), 0, 0, 0};
874 sws_scale(swsContext, srcBuffers, srcLinesizes, 0, inputImage.height(), dstBuffers, dstLinesizes);
879 if (i == 0 || ctx.
imageFormat() != outputPixelFormat) {
881 if (!ctx.
allocateImage(outputWidth, outputHeight, outputPixelFormat)) {
883 << int(outputPixelFormat);
888 const uint8_t *srcBuffers[] = {inputImage.bits(),
nullptr,
nullptr,
nullptr};
889 const int srcLinesizes[] = {inputImage.bytesPerLine(), 0, 0, 0};
890 sws_scale(swsContext,
899 av_image_copy2(dstBuffers,
908 int commitResult = int(ctx.
encoder().callMethod<jint>(
"commitVideo",
"()I"));
909 if (ctx.
checkResult(QStringLiteral(
"commitVideo"), commitResult)) {
913 if (audio && !audio->isFinished()) {
914 int pullFrameResult = audio->pullFrame(ctx);
919 return prepareAudioResult;
922 if (!audio->pushSamples(ctx)) {
925 }
while (audio->isSampleDataRemaining());
926 audio->finishFrame();
931 return prepareAudioResult;
934 if (!audio->finish(ctx)) {
954 return prepareVideoResult;
958 int finishVideoResult = int(ctx.
encoder().callMethod<jint>(
"finishVideo",
"()I"));
959 if (ctx.
checkResult(QStringLiteral(
"finishVideo"), finishVideoResult)) {
964 if (audio && !audio->isFinished()) {
967 return prepareAudioResult;
970 if (!audio->finish(ctx)) {
977 bool videoStreamEnded =
false;
978 bool audioStreamEnded = !audio;
980 if (!videoStreamEnded) {
981 int drainVideoResult =
drainVideo(ctx, 1000000LL);
983 videoStreamEnded =
true;
992 if (!audioStreamEnded) {
993 int drainAudioResult =
drainAudio(ctx, 1000000LL);
995 audioStreamEnded =
true;
1004 }
while (!videoStreamEnded || !audioStreamEnded);
1008 int closeResult = int(ctx.
encoder().callMethod<jint>(
"close",
"()I"));
1009 if (ctx.
checkResult(QStringLiteral(
"close"), closeResult)) {
1018 QString copyErrorMessage;
1041 const char *methodName = audio ?
"prepareAudio" :
"prepareVideo";
1042 QString title = audio ? QStringLiteral(
"prepareAudio") : QStringLiteral(
"prepareVideo");
1049 int prepareResult = int(ctx.
encoder().callMethod<jint>(methodName,
"(J)I", jlong(100000LL)));
1054 int drainResult =
drain(ctx, 0LL, audio);
1057 .arg(audio ? QStringLiteral(
"audio") : QStringLiteral(
"video")));
1077 return drain(ctx, initialTimeout,
false);
1082 return drain(ctx, initialTimeout,
true);
1087 const char *methodName = audio ?
"drainAudio" :
"drainVideo";
1088 QString title = audio ? QStringLiteral(
"drainAudio") : QStringLiteral(
"drainVideo");
1091 long long timeout = initialTimeout;
1097 int drainResult = int(ctx.
encoder().callMethod<jint>(methodName,
"(J)I", jlong(timeout)));
1127 ctx.
encoder().callObjectMethod(
"getInputImagePlaneBuffer",
"(I)Ljava/nio/ByteBuffer;", jint(index));
1128 if (ctx.
checkObject(QStringLiteral(
"plane"), plane)) {
1132 uint8_t *buffer =
static_cast<uint8_t *
>(ctx.
env()->GetDirectBufferAddress(plane.object<jobject>()));
1144 jint rowStride = ctx.
encoder().callMethod<jint>(
"getInputImagePlaneRowStride",
"(I)I", jint(index));
1147 }
else if (rowStride <= 0) {
1152 outRowStride = int(rowStride);
1158 jint pixelStride = ctx.
encoder().callMethod<jint>(
"getInputImagePlanePixelStride",
"(I)I", jint(index));
1161 }
else if (pixelStride <= 0) {
1166 outPixelStride = int(pixelStride);
1172 QJniObject audioBuffer = ctx.
encoder().callObjectMethod(
"getInputAudioBuffer",
"()Ljava/nio/ByteBuffer;", jint());
1173 if (ctx.
checkObject(QStringLiteral(
"audioBuffer"), audioBuffer)) {
1177 void *buffer = ctx.
env()->GetDirectBufferAddress(audioBuffer.object<jobject>());
1183 jint size = audioBuffer.callMethod<jint>(
"remaining",
"()I");
1205 const QPair<QVector<Format::Encoder> *,
const char *> ps[] = {
1206 {&videoEncoders,
"getSupportsForVideoFormat"},
1207 {&audioEncoders,
"getSupportsForAudioFormat"},
1211 QJniObject supports = QJniObject::callStaticObjectMethod(
"org/krita/android/VideoEncoder",
1213 "(I)Ljava/util/List;",
1215 if (ctx.
checkObject(QStringLiteral(
"supports"), supports)) {
1219 jint count = supports.callMethod<jint>(
"size",
"()I");
1224 for (jint i = 0; i < count; ++i) {
1225 QJniObject entry = supports.callObjectMethod(
"get",
"(I)Ljava/lang/Object;", i);
1226 if (ctx.
checkObject(QStringLiteral(
"entry"), entry)) {
1230 QJniObject name = entry.getObjectField(
"name",
"Ljava/lang/String;");
1231 if (ctx.
checkObject(QStringLiteral(
"name"), name)) {
1235 QString nameString = name.toString();
1236 if (ctx.
checkException(QStringLiteral(
"nameString")) || nameString.isEmpty()) {
1240 bool hardware = entry.getField<jboolean>(
"hardware");
1245 p.first->append({nameString, hardware});
1249 if (!videoEncoders.isEmpty()) {
1250 outSupportedFormats.append(
new Format(formatId, videoEncoders, audioEncoders));
1258 QFormLayout *form =
new QFormLayout(
this);
1299 for (
int i = 0; i < count; ++i) {
1327 for (
int i = 0; i < count; ++i) {
bool checkException(const QString &title)
bool checkObject(const QString &title, QJniObject &obj)
Context(QString *outErrorMessage=nullptr)
static bool isErrorResult(int result)
bool checkResult(const QString &title, int result)
void setEncoder(const QJniObject &encoder)
uint8_t ** imageBuffers()
bool allocateImage(int outputWidth, int outputHeight, AVPixelFormat outputFormat)
AVPixelFormat m_imageFormat
uint8_t * m_imageBuffers[4]
void setErrorMessage(const QString &errorMessage)
SwsContext * getSwsContextFor(int inputWidth, int inputHeight, AVPixelFormat inputFormat, int outputWidth, int outputHeight, AVPixelFormat outputFormat, int flags)
void setInternalErrorMessage(const QString &detail)
int getSwsFlags(const QString &scaleFilter) const
bool convertFrame(QImage &inOutImage, AVPixelFormat &outPixelFormat) const
Encoder * encoder(Imf::OutputFile &file, const ExrPaintLayerSaveInfo &info, int width)
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
#define KIS_SAFE_ASSERT_RECOVER_NOOP(cond)
bool copyFile(const QString &inputPath, const QString &outputPath, QString *outErrorMessage)