Krita Source Code Documentation
Loading...
Searching...
No Matches
KisAndroidMediaEncoderRunnable.cpp
Go to the documentation of this file.
1/*
2 * SPDX-License-Identifier: GPL-3.0-or-later
3 */
5
6#include <QComboBox>
7#include <QDir>
8#include <QFile>
9#include <QFormLayout>
10#include <QImage>
11#include <QSpinBox>
12#include <QTemporaryFile>
13#include <memory>
14
15#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
16#include <QJniEnvironment>
17#include <QJniObject>
18#else
19#include <QAndroidJniEnvironment>
20#include <QAndroidJniObject>
21using QJniEnvironment = QAndroidJniEnvironment;
22using QJniObject = QAndroidJniObject;
23#endif
24
25#include <klocalizedstring.h>
26
27#include <KisAndroidUtils.h>
28#include <kis_debug.h>
29
30#include <mlt++/Mlt.h>
31
32extern "C" {
33#include <libavutil/imgutils.h>
34#include <libavutil/pixfmt.h>
35#include <libswscale/swscale.h>
36}
37
39
50
52{
53public:
54 struct Encoder {
55 QString name;
57 };
58
59 Format(int formatId, const QVector<Encoder> &videoEncoders, const QVector<Encoder> &audioEncoders)
62 {
63 fillEncoders(m_videoEncoders, videoEncoders);
64 fillEncoders(m_audioEncoders, audioEncoders);
65 }
66
67 int formatId() const
68 {
69 return m_formatId;
70 }
71
72 Type type() const override
73 {
75 }
76
77 QString key() const override
78 {
80 }
81
82 QString title() const override
83 {
84 return i18n("Android: %1", titleForFormatId(m_formatId));
85 }
86
87 QString extension() const override
88 {
90 }
91
92 bool supportsAudio() const override
93 {
94 return !m_audioEncoders.isEmpty();
95 }
96
97 QWidget *createPreferencesWidget(const QVariantMap &preferences) const override
98 {
100
101 for (const Encoder &videoEncoder : m_videoEncoders) {
102 pw->addVideoEncoderOption(makeEncoderTitle(videoEncoder), videoEncoder.name);
103 }
104
105 for (const Encoder &audioEncoder : m_audioEncoders) {
106 pw->addAudioEncoderOption(makeEncoderTitle(audioEncoder), audioEncoder.name);
107 }
108
109 applyPreferencesToWidget(pw, preferences);
110 return pw;
111 }
112
113 void resetPreferencesWidget(QWidget *widget) const override
114 {
115 KisAndroidMediaEncoderPreferencesWidget *pw = qobject_cast<KisAndroidMediaEncoderPreferencesWidget *>(widget);
117 applyPreferencesToWidget(pw, QVariantMap());
118 }
119
120 QVariantMap getPreferencesFromWidget(QWidget *widget) const override
121 {
122 QVariantMap preferences;
123 KisAndroidMediaEncoderPreferencesWidget *pw = qobject_cast<KisAndroidMediaEncoderPreferencesWidget *>(widget);
125 preferences.insert(QStringLiteral("encoder"), pw->videoEncoder());
126 preferences.insert(QStringLiteral("bitrate"), pw->videoBitrate());
127 if (supportsAudio()) {
128 preferences.insert(QStringLiteral("aencoder"), pw->audioEncoder());
129 preferences.insert(QStringLiteral("abitrate"), pw->audioBitrate());
130 }
131 return preferences;
132 }
133
134 QString getVideoEncoderPreference(const QVariantMap &preferences) const
135 {
136 QString videoEncoder = preferences.value(QStringLiteral("encoder")).toString();
137 if (videoEncoder.isEmpty() && !m_videoEncoders.isEmpty()) {
138 return m_videoEncoders.constFirst().name;
139 } else {
140 return videoEncoder;
141 }
142 }
143
144 int getVideoBitratePreference(const QVariantMap &preferences) const
145 {
146 int videoBitrate = preferences.value(QStringLiteral("bitrate")).toInt();
147 if (videoBitrate <= 0) {
149 } else {
150 return videoBitrate;
151 }
152 }
153
154 QString getAudioEncoderPreference(const QVariantMap &preferences) const
155 {
156 QString audioEncoder = preferences.value(QStringLiteral("aencoder")).toString();
157 if (audioEncoder.isEmpty() && !m_videoEncoders.isEmpty()) {
158 return m_audioEncoders.constFirst().name;
159 } else {
160 return audioEncoder;
161 }
162 }
163
164 int getAudioBitratePreference(const QVariantMap &preferences) const
165 {
166 int audioBitrate = preferences.value(QStringLiteral("abitrate")).toInt();
167 if (audioBitrate <= 0) {
169 } else {
170 return audioBitrate;
171 }
172 }
173
174 int audioSampleRate() const
175 {
176 // While creating an audio track on Android lets you pass it a sample
177 // rate, that doesn't actually give you an audio track with the given
178 // sample rate. For WEBM with Vorbis, you always get a 48kHz track. MP4
179 // with AAC seems more forgiving, but let's go with known good values.
180 switch (m_formatId) {
181 case FORMAT_MP4_H264:
182 case FORMAT_MP4_AV1:
183 return 44100;
184 case FORMAT_WEBM_VP8:
185 return 48000;
186 }
187 return 44100;
188 }
189
190private:
191 static void fillEncoders(QVector<Encoder> &outEncoders, const QVector<Encoder> &encoders)
192 {
193 // Prefer software encoders, because hardware encoders are often busted.
194 outEncoders.reserve(encoders.size());
195 for (const Encoder &encoder : encoders) {
196 if (!encoder.hardware) {
197 outEncoders.append(encoder);
198 }
199 }
200 for (const Encoder &encoder : encoders) {
201 if (encoder.hardware) {
202 outEncoders.append(encoder);
203 }
204 }
205 }
206
207 static QString makeEncoderTitle(const Encoder &encoder)
208 {
209 if (encoder.hardware) {
210 return i18n("%1 (hardware)", encoder.name);
211 } else {
212 return i18n("%1 (software)", encoder.name);
213 }
214 }
215
216 void applyPreferencesToWidget(KisAndroidMediaEncoderPreferencesWidget *pw, const QVariantMap &preferences) const
217 {
220 if (supportsAudio()) {
223 }
224 }
225
226 static QString keyForFormatId(int formatId)
227 {
228 switch (formatId) {
229 case FORMAT_MP4_H264:
230 return QStringLiteral("android:mp4:h264");
231 case FORMAT_WEBM_VP8:
232 return QStringLiteral("android:webm:vp8");
233 case FORMAT_MP4_AV1:
234 return QStringLiteral("android:mp4:av1");
235 }
236 return QString();
237 }
238
239 static QString titleForFormatId(int formatId)
240 {
241 switch (formatId) {
242 case FORMAT_MP4_H264:
243 return QStringLiteral("MP4/H.264");
244 case FORMAT_WEBM_VP8:
245 return QStringLiteral("WEBM/VP8");
246 case FORMAT_MP4_AV1:
247 return QStringLiteral("MP4/AV1");
248 }
249 return QString();
250 }
251
252 static QString extensionForFormatId(int formatId)
253 {
254 switch (formatId) {
255 case FORMAT_MP4_H264:
256 case FORMAT_MP4_AV1:
257 return QStringLiteral("mp4");
258 case FORMAT_WEBM_VP8:
259 return QStringLiteral("webm");
260 }
261 return QString();
262 }
263
265 {
266 switch (formatId) {
267 case FORMAT_MP4_H264:
268 return 6000000;
269 case FORMAT_WEBM_VP8:
270 return 7000000;
271 case FORMAT_MP4_AV1:
272 return 3200000;
273 }
274 return 6000000;
275 }
276
278 {
279 switch (formatId) {
280 case FORMAT_MP4_H264:
281 case FORMAT_MP4_AV1:
282 return 128000;
283 case FORMAT_WEBM_VP8:
284 return 96000;
285 }
286 return 128000;
287 }
288
292};
293
295{
296public:
297 explicit Context(QString *outErrorMessage = nullptr)
298 : KisLibavEncoderContext(outErrorMessage)
299 {
300 }
301
302 ~Context() override
303 {
304 clearEncoder();
305 }
306
307 QJniEnvironment &env()
308 {
309 return m_env;
310 }
311
312 QJniObject &encoder()
313 {
314 return m_encoder;
315 }
316
317 void setEncoder(const QJniObject &encoder)
318 {
320 }
321
323 {
324 if (m_encoder.isValid()) {
325 m_encoder.callMethod<void>("cancel", "()V");
326 m_encoder = QJniObject();
327 if (m_env->ExceptionCheck()) {
328 warnFile << "JNI exception occurred cancelling encoder";
329 m_env->ExceptionDescribe();
330 m_env->ExceptionClear();
331 }
332 }
333 }
334
335 bool checkObject(const QString &title, QJniObject &obj)
336 {
337 if (checkException(title)) {
338 return true;
339 } else if (!obj.isValid()) {
340 setInternalErrorMessage(QStringLiteral("JNI object %1 invalid").arg(title));
341 return true;
342 } else {
343 return false;
344 }
345 }
346
347 bool checkResult(const QString &title, int result)
348 {
349 if (checkException(title)) {
350 return true;
351 } else if (result == STATUS_ERROR_START_VIDEO_ENCODER) {
352 warnFile << "Start video encoder error" << result;
353 setErrorMessage(i18n("Unsupported video parameters, try lowering the video FPS or size"));
354 return true;
355 } else if (result == STATUS_ERROR_START_AUDIO_FORMAT || result == STATUS_ERROR_START_AUDIO_ENCODER) {
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"));
360 return true;
363 warnFile << "Muxer track error" << result;
364 setErrorMessage(i18n("Unsupported format"));
365 return true;
366 } else if (isErrorResult(result)) {
367 setInternalErrorMessage(QStringLiteral("%1 failed with code %2").arg(title).arg(result));
368 return true;
369 } else {
370 return false;
371 }
372 }
373
374 bool checkException(const QString &title)
375 {
376 if (m_env->ExceptionCheck()) {
377 setInternalErrorMessage(QStringLiteral("JNI exception in %1").arg(title));
378 m_env->ExceptionDescribe();
379 m_env->ExceptionClear();
380 return true;
381 } else {
382 return false;
383 }
384 }
385
386 int imageFormat() const
387 {
388 return m_imageFormat;
389 }
390
391 uint8_t **imageBuffers()
392 {
393 return m_imageBuffers;
394 }
395
397 {
398 return m_imageLinesizes;
399 }
400
401 bool allocateImage(int outputWidth, int outputHeight, AVPixelFormat outputFormat)
402 {
403 // The Android encoder really shouldn't be changing
404 // pixel formats along the way, but just in case.
405 if (m_imageFormat != AV_PIX_FMT_NONE) {
406 m_imageFormat = AV_PIX_FMT_NONE;
407 av_freep(&m_imageBuffers[0]);
408 }
409
410 int result = av_image_alloc(m_imageBuffers, m_imageLinesizes, outputWidth, outputHeight, outputFormat, 32);
411 if (result >= 0) {
412 m_imageFormat = outputFormat;
413 return true;
414 } else {
415 setInternalErrorMessage(QStringLiteral("av_image_alloc error %1").arg(result));
416 return false;
417 }
418 }
419
420private:
421 static bool isErrorResult(int result)
422 {
423 return result >= 100;
424 }
425
426 QJniEnvironment m_env;
427 QJniObject m_encoder;
428 uint8_t *m_imageBuffers[4] = {nullptr, nullptr, nullptr, nullptr};
429 int m_imageLinesizes[4] = {0, 0, 0, 0};
430 AVPixelFormat m_imageFormat = AV_PIX_FMT_NONE;
431};
432
434{
435public:
436 static constexpr int PULL_FRAME_OK = 0;
437 static constexpr int PULL_FRAME_END_OF_STREAM = 1;
438 static constexpr int PULL_FRAME_ERROR = 2;
439
440 Audio(mlt_audio_format format, int sampleRate, int channelCount)
441 : m_format(format)
444 , m_sampleSize(mlt_audio_format_size(format, 1, 1))
445 {
446 }
447
448 QString formatName() const
449 {
450 return QString::fromUtf8(mlt_audio_format_name(m_format));
451 }
452
453 int sampleRate() const
454 {
455 return m_sampleRate;
456 }
457
458 int channelCount() const
459 {
460 return m_channelCount;
461 }
462
463 bool isFinished() const
464 {
465 return m_finished;
466 }
467
469 {
471
472 m_profile = std::make_unique<Mlt::Profile>();
473 m_profile->set_frame_rate(settings.outputFps, 1);
474
475 m_producer = std::make_unique<Mlt::Producer>(*m_profile, "avformat", m_audioFileBytes.constData());
476 if (!m_producer->is_valid()) {
477 ctx.setInternalErrorMessage(QStringLiteral("failed to open MLT producer for '%1'").arg(settings.audioFile));
478 return false;
479 }
480
481 m_filter = std::make_unique<Mlt::Filter>(*m_profile, "swresample");
482 if (!m_filter->is_valid()) {
483 ctx.setInternalErrorMessage(QStringLiteral("failed to create MLT filter"));
484 return false;
485 }
486
487 int result = m_producer->attach(*m_filter);
488 if (result != 0) {
489 ctx.setInternalErrorMessage(QStringLiteral("error %1 attaching MLT filter").arg(result));
490 return false;
491 }
492
494
495 return true;
496 }
497
499 {
500 if (m_frame) {
501 return PULL_FRAME_OK;
502 }
503
504 m_frame.reset(m_producer->get_frame());
505 if (!m_frame || !m_frame->is_valid()) {
506 m_frame.reset();
508 }
509
510 mlt_audio_format format = m_format;
513
514 float fps = float(m_profile->fps());
515 int64_t position = m_frame->get_position();
516 int sampleCount = mlt_audio_calculate_frame_samples(fps, sampleRate, position);
517 const void *sampleData = m_frame->get_audio(format, sampleRate, channelCount, sampleCount);
518 if (sampleCount <= 0 || !sampleData) {
519 m_frame.reset();
521 }
522
523 if (format != m_format || sampleRate != m_sampleRate || channelCount != m_channelCount) {
524 ctx.setInternalErrorMessage(QStringLiteral("bad MLT frame format %1:%2:%3 != not %4:%5:%6")
525 .arg(int(format))
526 .arg(sampleRate)
527 .arg(channelCount)
528 .arg(int(m_format))
529 .arg(m_sampleRate)
530 .arg(m_channelCount));
531 m_frame.reset();
532 return PULL_FRAME_ERROR;
533 }
534
535 m_sampleCount = sampleCount;
536 m_sampleData = reinterpret_cast<const unsigned char *>(sampleData);
537 m_samplePos = 0;
538 return PULL_FRAME_OK;
539 }
540
542 {
543 void *buffer;
544 int availableSize;
545 if (!readAudioInputBuffer(ctx, buffer, availableSize)) {
546 return false;
547 }
548
549 int availableSamples = sizeToSamples(availableSize);
550 if (availableSamples <= 0) {
551 ctx.setInternalErrorMessage(QStringLiteral("no available samples from size %1").arg(availableSize));
552 return false;
553 }
554
555 int remainingSamples = m_sampleCount - m_samplePos;
556 int chunkSamples = qMin(availableSamples, remainingSamples);
557 int chunkSize = samplesToSize(chunkSamples);
558 memcpy(buffer, m_sampleData + samplesToSize(m_samplePos), chunkSize);
559
560 int commitResult =
561 int(ctx.encoder().callMethod<jint>("commitAudio", "(II)I", jint(chunkSamples), jint(chunkSize)));
562 if (ctx.checkResult(QStringLiteral("commitAudio"), commitResult)) {
563 return false;
564 }
565
566 m_samplePos += chunkSamples;
567 return true;
568 }
569
571 {
572 return m_samplePos < m_sampleCount;
573 }
574
576 {
577 m_frame.reset();
578 m_sampleData = nullptr;
579 m_sampleCount = 0;
580 m_samplePos = 0;
581 }
582
583 bool finish(Context &ctx)
584 {
585 if (!m_finished) {
586 int finishAudioResult = int(ctx.encoder().callMethod<jint>("finishAudio", "()I"));
587 if (ctx.checkResult(QStringLiteral("finishAudio"), finishAudioResult)) {
588 return false;
589 }
590 m_finished = true;
591 }
592 return true;
593 }
594
595private:
596 int sizeToSamples(int size) const
597 {
598 return size / (m_channelCount * m_sampleSize);
599 }
600
601 int samplesToSize(int samples) const
602 {
603 return samples * m_channelCount * m_sampleSize;
604 }
605
607 std::unique_ptr<Mlt::Profile> m_profile;
608 std::unique_ptr<Mlt::Producer> m_producer;
609 std::unique_ptr<Mlt::Filter> m_filter;
610 std::unique_ptr<Mlt::Frame> m_frame;
611 const unsigned char *m_sampleData = nullptr;
612 const mlt_audio_format m_format;
613 const int m_sampleRate;
614 const int m_channelCount;
615 const int m_sampleSize;
617 int m_samplePos = 0;
618 bool m_finished = false;
619};
620
622 QObject *parent)
623{
625 return new KisAndroidMediaEncoderRunnable(settings, parent);
626 } else {
627 return nullptr;
628 }
629}
630
632{
633 Context ctx;
634 int formatIds[] = {FORMAT_MP4_H264, FORMAT_WEBM_VP8, FORMAT_MP4_AV1};
635 for (int formatId : formatIds) {
636 checkFormatSupport(ctx, formatId, outSupportedFormats);
637 }
638}
639
645
647{
648 Format *format = static_cast<Format *>(settings().format);
649
650 QTemporaryFile tempFile;
651 QString tempFilePath;
652 if (tempFile.open()) {
653 tempFilePath = tempFile.fileName();
654 tempFile.close();
655 } else {
656 warnFile << "Failed to open temporary file:" << tempFile.errorString();
657 // Keep going, we might not actually need a temporary file.
658 }
659
660 Context ctx(&outErrorMessage);
661 int outputWidth = settings().outputSize.width();
662 int outputHeight = settings().outputSize.height();
663
664 // Set up audio if we were given some.
665 std::unique_ptr<Audio> audio;
666 if (!settings().audioFile.isEmpty()) {
667 // We always use these fixed parameters: 16 bit PCM, 2 channels and a
668 // sample rate according to the format. The Android media encoder makes
669 // it appear like you can create audio tracks with other parameters, but
670 // if it doesn't like the parameters it just gives you something else,
671 // which obviously just results in nonsense. We'll go with known good
672 // parameters instead, they'll also play back consistently everywhere.
673 audio = std::make_unique<Audio>(mlt_audio_s16, format->audioSampleRate(), 2);
674 if (!audio->open(ctx, settings())) {
676 }
677
678 switch (audio->pullFrame(ctx)) {
680 break;
682 // There's no frames, which can happen if our animation range start
683 // is beyond the end of the file. Just export without audio then.
684 audio.reset();
685 break;
686 default:
688 }
689 }
690
691 // Set up the encoder.
692 {
693 QJniObject outputPath = QJniObject::fromString(settings().outputFile);
694 if (ctx.checkObject(QStringLiteral("outputPath"), outputPath)) {
696 }
697
698 QJniObject tempPath = QJniObject::fromString(tempFilePath);
699 if (ctx.checkObject(QStringLiteral("tempPath"), tempPath)) {
701 }
702
703 QJniObject videoEncoderName =
704 QJniObject::fromString(format->getVideoEncoderPreference(settings().formatPreferences));
705 ctx.checkException(QStringLiteral("videoEncoderName"));
706
707 jint audioSampleRate;
708 jint audioChannelCount;
709 QJniObject audioEncoderName;
710 QJniObject audioFormatName;
711 if (audio) {
712 audioSampleRate = jint(audio->sampleRate());
713 audioChannelCount = jint(audio->channelCount());
714 audioEncoderName = QJniObject::fromString(format->getAudioEncoderPreference(settings().formatPreferences));
715 ctx.checkException(QStringLiteral("audioEncoderName"));
716 audioFormatName = QJniObject::fromString(audio->formatName());
717 ctx.checkException(QStringLiteral("audioFormatName"));
718 } else {
719 audioSampleRate = jint(0);
720 audioChannelCount = jint(0);
721 }
722
723 ctx.setEncoder(QJniObject(
724 "org/krita/android/VideoEncoder",
725 "(IIIFLjava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;III)V",
726 jint(format->formatId()),
727 jint(outputWidth),
728 jint(outputHeight),
729 jfloat(settings().outputFps),
730 outputPath.object<jstring>(),
731 tempPath.object<jstring>(),
732 videoEncoderName.object<jstring>(),
733 jint(format->getVideoBitratePreference(settings().formatPreferences)),
734 audio ? audioEncoderName.object<jstring>() : nullptr,
735 audio ? audioFormatName.object<jstring>() : nullptr,
736 jint(audioSampleRate),
737 jint(audioChannelCount),
738 jint(format->getAudioBitratePreference(settings().formatPreferences))));
739 if (ctx.checkObject(QStringLiteral("encoder"), ctx.encoder())) {
741 }
742 }
743
744 if (isCancelled()) {
746 }
747
748 // Start the encoding.
749 {
750 QJniObject activity = QJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative",
751 "activity",
752 "()Landroid/app/Activity;");
753 if (ctx.checkObject(QStringLiteral("activity"), activity)) {
755 }
756
757 int startResult =
758 int(ctx.encoder().callMethod<jint>("start", "(Landroid/content/Context;)I", activity.object<jobject>()));
759 if (ctx.checkResult(QStringLiteral("start"), startResult)) {
761 }
762 }
763
764 // Encode the frames.
765 Frame frame;
766 int swsFlags = ctx.getSwsFlags(settings().scaleFilter);
767 while (nextFrame(frame)) {
768 if (isCancelled()) {
770 }
771
772 // Grab the next frame from disk.
773 QImage inputImage;
774 if (!frame.readImage(inputImage)) {
775 // If we don't have audio, we can just skip the frame and hope it's
776 // okay. In timelapses it often is, you're not gonna notice a single
777 // frame missing in the video. However, if we have an animation with
778 // audio, skipping a frame would make a mess and be hard to resync,
779 // so in that case we just give up and bail out.
780 if (audio) {
781 ctx.setInternalErrorMessage(QStringLiteral("failed to read frame %1").arg(frame.path()));
783 } else {
784 continue;
785 }
786 }
787
788 AVPixelFormat inputPixelFormat;
789 if (!ctx.convertFrame(inputImage, inputPixelFormat)) {
790 // Dito to the above, try to skip frames we don't understand, don't
791 // bother when there's audio involved.
792 if (audio) {
793 ctx.setInternalErrorMessage(QStringLiteral("failed to convert frame %1").arg(frame.path()));
795 } else {
796 continue;
797 }
798 }
799
800 int instances = frame.instances();
801 for (int i = 0; i < instances; ++i) {
802 // Grab a buffer from the encoder.
803 EncodeResult prepareVideoResult = prepareVideo(ctx);
804 if (prepareVideoResult != EncodeResult::Completed) {
805 return prepareVideoResult;
806 }
807
808 // Retrieve the buffer layout.
809 EncoderImage encoderImage;
810 if (!readEncoderImage(ctx, encoderImage)) {
812 }
813
814 // Map the buffer layout to a libswscale-befitting arrangement. The
815 // layout may either have the YUV components in separate buffers or it
816 // may have the U and V components combined into a single buffer, where
817 // either U or V can come first. Which one we get depends on hardware.
818 uint8_t *dstBuffers[4] = {nullptr, nullptr, nullptr, nullptr};
819 int dstLinesizes[4] = {0, 0, 0, 0};
820 AVPixelFormat outputPixelFormat;
821 if (encoderImage.pixelStrideU == 1 && encoderImage.pixelStrideV == 1) {
822 // Separate Y, U and V buffers.
823 outputPixelFormat = AV_PIX_FMT_YUV420P;
824 dstBuffers[0] = encoderImage.bufferY;
825 dstBuffers[1] = encoderImage.bufferU;
826 dstBuffers[2] = encoderImage.bufferV;
827 dstLinesizes[0] = encoderImage.rowStrideY;
828 dstLinesizes[1] = encoderImage.rowStrideU;
829 dstLinesizes[2] = encoderImage.rowStrideV;
830
831 } else if (encoderImage.pixelStrideU == 2 && encoderImage.pixelStrideV == 2
832 && encoderImage.bufferU + 1 == encoderImage.bufferV) {
833 // One Y buffer and one combined UV buffer, U comes first.
834 outputPixelFormat = AV_PIX_FMT_NV12;
835 dstBuffers[0] = encoderImage.bufferY;
836 dstBuffers[1] = encoderImage.bufferU;
837 dstLinesizes[0] = encoderImage.rowStrideY;
838 dstLinesizes[1] = encoderImage.rowStrideU;
839
840 } else if (encoderImage.pixelStrideU == 2 && encoderImage.pixelStrideV == 2
841 && encoderImage.bufferV + 1 == encoderImage.bufferU) {
842 // One Y buffer and one combined UV buffer, V comes first.
843 outputPixelFormat = AV_PIX_FMT_NV21;
844 dstBuffers[0] = encoderImage.bufferY;
845 dstBuffers[1] = encoderImage.bufferV;
846 dstLinesizes[0] = encoderImage.rowStrideY;
847 dstLinesizes[1] = encoderImage.rowStrideV;
848
849 } else {
850 ctx.setInternalErrorMessage(QStringLiteral("unknown buffer format u%1/%2 v%3/%4")
851 .arg(quintptr(encoderImage.bufferU), 0, 16)
852 .arg(encoderImage.pixelStrideU)
853 .arg(quintptr(encoderImage.bufferV), 0, 16)
854 .arg(encoderImage.pixelStrideV));
856 }
857
858 SwsContext *swsContext = ctx.getSwsContextFor(inputImage.width(),
859 inputImage.height(),
860 inputPixelFormat,
861 outputWidth,
862 outputHeight,
863 outputPixelFormat,
864 swsFlags);
865 if (!swsContext) {
866 ctx.setInternalErrorMessage(QStringLiteral("sws_getCachedContext"));
868 }
869
870 if (instances == 1) {
871 // Just a single frame, scale it into the native buffer.
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);
875
876 } else {
877 // Repeated frame, scale it into an intermediate buffer, then
878 // copy it over to the native one for each instance.
879 if (i == 0 || ctx.imageFormat() != outputPixelFormat) {
880 if (ctx.imageFormat() != outputPixelFormat) {
881 if (!ctx.allocateImage(outputWidth, outputHeight, outputPixelFormat)) {
882 warnFile << "Encoder changed pixel format from" << int(ctx.imageFormat()) << "to"
883 << int(outputPixelFormat);
885 }
886 }
887
888 const uint8_t *srcBuffers[] = {inputImage.bits(), nullptr, nullptr, nullptr};
889 const int srcLinesizes[] = {inputImage.bytesPerLine(), 0, 0, 0};
890 sws_scale(swsContext,
891 srcBuffers,
892 srcLinesizes,
893 0,
894 inputImage.height(),
895 ctx.imageBuffers(),
896 ctx.imageLinesizes());
897 }
898
899 av_image_copy2(dstBuffers,
900 dstLinesizes,
901 ctx.imageBuffers(),
902 ctx.imageLinesizes(),
903 outputPixelFormat,
904 outputWidth,
905 outputHeight);
906 }
907
908 int commitResult = int(ctx.encoder().callMethod<jint>("commitVideo", "()I"));
909 if (ctx.checkResult(QStringLiteral("commitVideo"), commitResult)) {
911 }
912
913 if (audio && !audio->isFinished()) {
914 int pullFrameResult = audio->pullFrame(ctx);
915 if (pullFrameResult == Audio::PULL_FRAME_OK) {
916 do {
917 EncodeResult prepareAudioResult = prepareAudio(ctx);
918 if (prepareAudioResult != EncodeResult::Completed) {
919 return prepareAudioResult;
920 }
921
922 if (!audio->pushSamples(ctx)) {
924 }
925 } while (audio->isSampleDataRemaining());
926 audio->finishFrame();
927
928 } else if (pullFrameResult == Audio::PULL_FRAME_END_OF_STREAM) {
929 EncodeResult prepareAudioResult = prepareAudio(ctx);
930 if (prepareAudioResult != EncodeResult::Completed) {
931 return prepareAudioResult;
932 }
933
934 if (!audio->finish(ctx)) {
936 }
937
938 } else {
940 }
941 }
942 }
943 }
944
945 if (isCancelled()) {
947 }
948
949 // Finish the encoder streams.
950 {
951 // Need a buffer from the encoder to tell it that it's done.
952 EncodeResult prepareVideoResult = prepareVideo(ctx);
953 if (prepareVideoResult != EncodeResult::Completed) {
954 return prepareVideoResult;
955 }
956
957 // Hand empty buffer back with the end of stream flag set.
958 int finishVideoResult = int(ctx.encoder().callMethod<jint>("finishVideo", "()I"));
959 if (ctx.checkResult(QStringLiteral("finishVideo"), finishVideoResult)) {
961 }
962
963 // Same procedure with audio, but it might already have finished.
964 if (audio && !audio->isFinished()) {
965 EncodeResult prepareAudioResult = prepareAudio(ctx);
966 if (prepareAudioResult != EncodeResult::Completed) {
967 return prepareAudioResult;
968 }
969
970 if (!audio->finish(ctx)) {
972 }
973 }
974 }
975
976 // Drain all remaining frames and samples out of the encoders.
977 bool videoStreamEnded = false;
978 bool audioStreamEnded = !audio;
979 do {
980 if (!videoStreamEnded) {
981 int drainVideoResult = drainVideo(ctx, 1000000LL);
982 if (drainVideoResult == DRAIN_END_OF_STREAM) {
983 videoStreamEnded = true;
984 } else if (drainVideoResult == DRAIN_ERROR) {
986 } else if (drainVideoResult == DRAIN_CANCELLED) {
988 } else {
989 KIS_SAFE_ASSERT_RECOVER_NOOP(drainVideoResult >= 0);
990 }
991 }
992 if (!audioStreamEnded) {
993 int drainAudioResult = drainAudio(ctx, 1000000LL);
994 if (drainAudioResult == DRAIN_END_OF_STREAM) {
995 audioStreamEnded = true;
996 } else if (drainAudioResult == DRAIN_ERROR) {
998 } else if (drainAudioResult == DRAIN_CANCELLED) {
1000 } else {
1001 KIS_SAFE_ASSERT_RECOVER_NOOP(drainAudioResult >= 0);
1002 }
1003 }
1004 } while (!videoStreamEnded || !audioStreamEnded);
1005
1006 // Close the encoder, copy the temporary file to the output file if needed.
1007 {
1008 int closeResult = int(ctx.encoder().callMethod<jint>("close", "()I"));
1009 if (ctx.checkResult(QStringLiteral("close"), closeResult)) {
1010 return EncodeResult::Failed;
1011 }
1012
1013 if (isCancelled()) {
1015 }
1016
1017 if (closeResult == STATUS_NEEDS_COPY) {
1018 QString copyErrorMessage;
1019 if (!KisAndroidUtils::copyFile(tempFilePath, settings().outputFile, &copyErrorMessage)) {
1020 ctx.setInternalErrorMessage(copyErrorMessage);
1021 return EncodeResult::Failed;
1022 }
1023 }
1024 }
1025
1027}
1028
1033
1038
1040{
1041 const char *methodName = audio ? "prepareAudio" : "prepareVideo";
1042 QString title = audio ? QStringLiteral("prepareAudio") : QStringLiteral("prepareVideo");
1043
1044 while (true) {
1045 if (isCancelled()) {
1047 }
1048
1049 int prepareResult = int(ctx.encoder().callMethod<jint>(methodName, "(J)I", jlong(100000LL)));
1050 if (ctx.checkResult(title, prepareResult)) {
1051 return EncodeResult::Failed;
1052
1053 } else if (prepareResult == STATUS_TIMEOUT) {
1054 int drainResult = drain(ctx, 0LL, audio);
1055 if (drainResult == DRAIN_END_OF_STREAM) {
1056 ctx.setInternalErrorMessage(QStringLiteral("unexpected end of %1 stream")
1057 .arg(audio ? QStringLiteral("audio") : QStringLiteral("video")));
1058 return EncodeResult::Failed;
1059 } else if (drainResult == DRAIN_ERROR) {
1060 return EncodeResult::Failed;
1061 } else if (drainResult == DRAIN_CANCELLED) {
1063 } else {
1064 KIS_SAFE_ASSERT_RECOVER_NOOP(drainResult >= 0);
1065 }
1066
1067 } else {
1068 KIS_SAFE_ASSERT_RECOVER_NOOP(prepareResult == STATUS_OK);
1069 break;
1070 }
1071 }
1073}
1074
1075int KisAndroidMediaEncoderRunnable::drainVideo(Context &ctx, long long initialTimeout)
1076{
1077 return drain(ctx, initialTimeout, false);
1078}
1079
1080int KisAndroidMediaEncoderRunnable::drainAudio(Context &ctx, long long initialTimeout)
1081{
1082 return drain(ctx, initialTimeout, true);
1083}
1084
1085int KisAndroidMediaEncoderRunnable::drain(Context &ctx, long long initialTimeout, bool audio)
1086{
1087 const char *methodName = audio ? "drainAudio" : "drainVideo";
1088 QString title = audio ? QStringLiteral("drainAudio") : QStringLiteral("drainVideo");
1089
1090 int count = 0;
1091 long long timeout = initialTimeout;
1092 while (true) {
1093 if (isCancelled()) {
1094 return DRAIN_CANCELLED;
1095 }
1096
1097 int drainResult = int(ctx.encoder().callMethod<jint>(methodName, "(J)I", jlong(timeout)));
1098
1099 if (ctx.checkResult(title, drainResult)) {
1100 return DRAIN_ERROR;
1101
1102 } else if (drainResult == STATUS_TIMEOUT) {
1103 break;
1104
1105 } else if (drainResult == STATUS_END_OF_STREAM) {
1106 return DRAIN_END_OF_STREAM;
1107
1108 } else {
1110 ++count;
1111 }
1112 }
1113 return count;
1114}
1115
1117{
1118 return readPlaneBuffer(ctx, 0, outImage.bufferY) && readPlaneBuffer(ctx, 1, outImage.bufferU)
1119 && readPlaneBuffer(ctx, 2, outImage.bufferV) && readPlaneRowStride(ctx, 0, outImage.rowStrideY)
1120 && readPlaneRowStride(ctx, 1, outImage.rowStrideU) && readPlaneRowStride(ctx, 2, outImage.rowStrideV)
1121 && readPlanePixelStride(ctx, 1, outImage.pixelStrideU) && readPlanePixelStride(ctx, 2, outImage.pixelStrideV);
1122}
1123
1124bool KisAndroidMediaEncoderRunnable::readPlaneBuffer(Context &ctx, int index, uint8_t *&outBuffer)
1125{
1126 QJniObject plane =
1127 ctx.encoder().callObjectMethod("getInputImagePlaneBuffer", "(I)Ljava/nio/ByteBuffer;", jint(index));
1128 if (ctx.checkObject(QStringLiteral("plane"), plane)) {
1129 return false;
1130 }
1131
1132 uint8_t *buffer = static_cast<uint8_t *>(ctx.env()->GetDirectBufferAddress(plane.object<jobject>()));
1133 if (!buffer) {
1134 ctx.setInternalErrorMessage(QStringLiteral("null plane buffer %1").arg(index));
1135 return false;
1136 }
1137
1138 outBuffer = buffer;
1139 return true;
1140}
1141
1142bool KisAndroidMediaEncoderRunnable::readPlaneRowStride(Context &ctx, int index, int &outRowStride)
1143{
1144 jint rowStride = ctx.encoder().callMethod<jint>("getInputImagePlaneRowStride", "(I)I", jint(index));
1145 if (ctx.checkException(QStringLiteral("rowStride"))) {
1146 return false;
1147 } else if (rowStride <= 0) {
1148 ctx.setInternalErrorMessage(QStringLiteral("invalid row stride %1: %2").arg(index).arg(rowStride));
1149 return false;
1150 }
1151
1152 outRowStride = int(rowStride);
1153 return true;
1154}
1155
1156bool KisAndroidMediaEncoderRunnable::readPlanePixelStride(Context &ctx, int index, int &outPixelStride)
1157{
1158 jint pixelStride = ctx.encoder().callMethod<jint>("getInputImagePlanePixelStride", "(I)I", jint(index));
1159 if (ctx.checkException(QStringLiteral("pixelStride"))) {
1160 return false;
1161 } else if (pixelStride <= 0) {
1162 ctx.setInternalErrorMessage(QStringLiteral("invalid pixel stride %1: %2").arg(index).arg(pixelStride));
1163 return false;
1164 }
1165
1166 outPixelStride = int(pixelStride);
1167 return true;
1168}
1169
1170bool KisAndroidMediaEncoderRunnable::readAudioInputBuffer(Context &ctx, void *&outBuffer, int &outSize)
1171{
1172 QJniObject audioBuffer = ctx.encoder().callObjectMethod("getInputAudioBuffer", "()Ljava/nio/ByteBuffer;", jint());
1173 if (ctx.checkObject(QStringLiteral("audioBuffer"), audioBuffer)) {
1174 return false;
1175 }
1176
1177 void *buffer = ctx.env()->GetDirectBufferAddress(audioBuffer.object<jobject>());
1178 if (!buffer) {
1179 ctx.setInternalErrorMessage(QStringLiteral("null audio buffer"));
1180 return false;
1181 }
1182
1183 jint size = audioBuffer.callMethod<jint>("remaining", "()I");
1184 if (ctx.checkException(QStringLiteral("remaining"))) {
1185 return false;
1186 }
1187
1188 if (size <= 0) {
1189 ctx.setInternalErrorMessage(QStringLiteral("audio buffer with size %1").arg(size));
1190 return false;
1191 }
1192
1193 outBuffer = buffer;
1194 outSize = size;
1195 return true;
1196}
1197
1199 int formatId,
1200 QVector<KisMediaEncoderFormat *> &outSupportedFormats)
1201{
1202 QVector<Format::Encoder> videoEncoders;
1203 QVector<Format::Encoder> audioEncoders;
1204
1205 const QPair<QVector<Format::Encoder> *, const char *> ps[] = {
1206 {&videoEncoders, "getSupportsForVideoFormat"},
1207 {&audioEncoders, "getSupportsForAudioFormat"},
1208 };
1209
1210 for (const QPair<QVector<Format::Encoder> *, const char *> &p : ps) {
1211 QJniObject supports = QJniObject::callStaticObjectMethod("org/krita/android/VideoEncoder",
1212 p.second,
1213 "(I)Ljava/util/List;",
1214 jint(formatId));
1215 if (ctx.checkObject(QStringLiteral("supports"), supports)) {
1216 return;
1217 }
1218
1219 jint count = supports.callMethod<jint>("size", "()I");
1220 if (ctx.checkException(QStringLiteral("size"))) {
1221 return;
1222 }
1223
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)) {
1227 continue;
1228 }
1229
1230 QJniObject name = entry.getObjectField("name", "Ljava/lang/String;");
1231 if (ctx.checkObject(QStringLiteral("name"), name)) {
1232 continue;
1233 }
1234
1235 QString nameString = name.toString();
1236 if (ctx.checkException(QStringLiteral("nameString")) || nameString.isEmpty()) {
1237 continue;
1238 }
1239
1240 bool hardware = entry.getField<jboolean>("hardware");
1241 if (ctx.checkException(QStringLiteral("hardware"))) {
1242 continue;
1243 }
1244
1245 p.first->append({nameString, hardware});
1246 }
1247 }
1248
1249 if (!videoEncoders.isEmpty()) {
1250 outSupportedFormats.append(new Format(formatId, videoEncoders, audioEncoders));
1251 }
1252}
1253
1255 QWidget *parent)
1256 : QWidget(parent)
1257{
1258 QFormLayout *form = new QFormLayout(this);
1259
1260 m_cmbVideoEncoder = new QComboBox;
1261 form->addRow(i18n("Video encoder:"), m_cmbVideoEncoder);
1262
1263 m_intVideoBitrate = new QSpinBox;
1264 m_intVideoBitrate->setRange(1, 999999999);
1265 form->addRow(i18n("Video bitrate:"), m_intVideoBitrate);
1266
1267 if (format->supportsAudio()) {
1268 m_cmbAudioEncoder = new QComboBox;
1269 form->addRow(i18n("Audio encoder:"), m_cmbAudioEncoder);
1270
1271 m_intAudioBitrate = new QSpinBox;
1272 m_intAudioBitrate->setRange(1, 999999999);
1273 form->addRow(i18n("Audio bitrate:"), m_intAudioBitrate);
1274 } else {
1275 m_cmbAudioEncoder = nullptr;
1276 m_intAudioBitrate = nullptr;
1277 }
1278}
1279
1280void KisAndroidMediaEncoderPreferencesWidget::addVideoEncoderOption(const QString &title, const QString &key)
1281{
1282 m_cmbVideoEncoder->addItem(title, QVariant(key));
1283}
1284
1285void KisAndroidMediaEncoderPreferencesWidget::addAudioEncoderOption(const QString &title, const QString &key)
1286{
1287 m_cmbAudioEncoder->addItem(title, QVariant(key));
1288}
1289
1291{
1292 return m_cmbVideoEncoder->currentData().toString();
1293}
1294
1296{
1297 int index = 0;
1298 int count = m_cmbVideoEncoder->count();
1299 for (int i = 0; i < count; ++i) {
1300 if (m_cmbVideoEncoder->itemData(i).toString() == key) {
1301 index = i;
1302 break;
1303 }
1304 }
1305 m_cmbVideoEncoder->setCurrentIndex(index);
1306}
1307
1312
1317
1319{
1320 return m_cmbAudioEncoder->currentData().toString();
1321}
1322
1324{
1325 int index = 0;
1326 int count = m_cmbAudioEncoder->count();
1327 for (int i = 0; i < count; ++i) {
1328 if (m_cmbAudioEncoder->itemData(i).toString() == key) {
1329 index = i;
1330 break;
1331 }
1332 }
1333 m_cmbAudioEncoder->setCurrentIndex(index);
1334}
1335
1340
const Params2D p
void addAudioEncoderOption(const QString &title, const QString &key)
void addVideoEncoderOption(const QString &title, const QString &key)
KisAndroidMediaEncoderPreferencesWidget(const KisMediaEncoderFormat *format, QWidget *parent=nullptr)
bool open(Context &ctx, const KisMediaEncoderWrapperSettings &settings)
Audio(mlt_audio_format format, int sampleRate, int channelCount)
bool checkObject(const QString &title, QJniObject &obj)
bool checkResult(const QString &title, int result)
bool allocateImage(int outputWidth, int outputHeight, AVPixelFormat outputFormat)
QWidget * createPreferencesWidget(const QVariantMap &preferences) const override
QString getAudioEncoderPreference(const QVariantMap &preferences) const
Format(int formatId, const QVector< Encoder > &videoEncoders, const QVector< Encoder > &audioEncoders)
static QString makeEncoderTitle(const Encoder &encoder)
static void fillEncoders(QVector< Encoder > &outEncoders, const QVector< Encoder > &encoders)
int getVideoBitratePreference(const QVariantMap &preferences) const
int getAudioBitratePreference(const QVariantMap &preferences) const
QString getVideoEncoderPreference(const QVariantMap &preferences) const
void applyPreferencesToWidget(KisAndroidMediaEncoderPreferencesWidget *pw, const QVariantMap &preferences) const
void resetPreferencesWidget(QWidget *widget) const override
QVariantMap getPreferencesFromWidget(QWidget *widget) const override
static void checkFormatSupport(Context &ctx, int formatId, QVector< KisMediaEncoderFormat * > &outSupportedFormats)
int drain(Context &ctx, long long initialTimeout, bool audio)
static constexpr int STATUS_ERROR_DRAIN_VIDEO_MUXER_ADD_TRACK
EncodeResult prepare(Context &ctx, bool audio)
static constexpr int STATUS_ERROR_DRAIN_AUDIO_MUXER_ADD_TRACK
static bool readEncoderImage(Context &ctx, EncoderImage &outImage)
static bool readAudioInputBuffer(Context &ctx, void *&outBuffer, int &outSize)
static void getSupportedFormats(QVector< KisMediaEncoderFormat * > &outSupportedFormats)
static bool readPlaneBuffer(Context &ctx, int index, uint8_t *&outBuffer)
KisAndroidMediaEncoderRunnable(const KisMediaEncoderWrapperSettings &settings, QObject *parent)
static bool readPlaneRowStride(Context &ctx, int index, int &outRowStride)
int drainAudio(Context &ctx, long long initialTimeout)
int drainVideo(Context &ctx, long long initialTimeout)
static KisAndroidMediaEncoderRunnable * create(const KisMediaEncoderWrapperSettings &settings, QObject *parent=nullptr)
static bool readPlanePixelStride(Context &ctx, int index, int &outPixelStride)
EncodeResult encode(QString &outErrorMessage) override
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
virtual bool supportsAudio() const =0
virtual Type type() const =0
bool readImage(QImage &outImage) const
const KisMediaEncoderWrapperSettings settings()
bool nextFrame(Frame &outFrame)
Encoder * encoder(Imf::OutputFile &file, const ExrPaintLayerSaveInfo &info, int width)
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:129
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define KIS_SAFE_ASSERT_RECOVER_NOOP(cond)
Definition kis_assert.h:130
#define warnFile
Definition kis_debug.h:99
bool copyFile(const QString &inputPath, const QString &outputPath, QString *outErrorMessage)