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
14#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
15#include <QJniEnvironment>
16#include <QJniObject>
17#else
18#include <QAndroidJniEnvironment>
19#include <QAndroidJniObject>
20using QJniEnvironment = QAndroidJniEnvironment;
21using QJniObject = QAndroidJniObject;
22#endif
23
24#include <klocalizedstring.h>
25
26#include <kis_debug.h>
27
28extern "C" {
29#include <libavutil/imgutils.h>
30#include <libavutil/pixfmt.h>
31#include <libswscale/swscale.h>
32}
33
44
46{
47public:
48 struct Encoder {
49 QString name;
51 };
52
53 Format(int formatId, const QVector<Encoder> &encoders)
56 {
57 // Prefer software encoders, because hardware encoders are often busted.
58 m_encoders.reserve(encoders.size());
59 for (const Encoder &encoder : encoders) {
60 if (!encoder.hardware) {
61 m_encoders.append(encoder);
62 }
63 }
64 for (const Encoder &encoder : encoders) {
65 if (encoder.hardware) {
66 m_encoders.append(encoder);
67 }
68 }
69 }
70
71 int formatId() const
72 {
73 return m_formatId;
74 }
75
76 Type type() const override
77 {
79 }
80
81 QString key() const override
82 {
84 }
85
86 QString title() const override
87 {
88 return i18n("Android: %1", titleForFormatId(m_formatId));
89 }
90
91 QString extension() const override
92 {
94 }
95
96 QWidget *createPreferencesWidget(const QVariantMap &preferences) const override
97 {
99
100 for (const Encoder &encoder : m_encoders) {
101 QString title;
102 if (encoder.hardware) {
103 title = i18n("%1 (hardware)", encoder.name);
104 } else {
105 title = i18n("%1 (software)", encoder.name);
106 }
108 }
109
110 applyPreferencesToWidget(pw, preferences);
111 return pw;
112 }
113
114 void resetPreferencesWidget(QWidget *widget) const override
115 {
116 KisAndroidMediaEncoderPreferencesWidget *pw = qobject_cast<KisAndroidMediaEncoderPreferencesWidget *>(widget);
118 applyPreferencesToWidget(pw, QVariantMap());
119 }
120
121 QVariantMap getPreferencesFromWidget(QWidget *widget) const override
122 {
123 QVariantMap preferences;
124 KisAndroidMediaEncoderPreferencesWidget *pw = qobject_cast<KisAndroidMediaEncoderPreferencesWidget *>(widget);
126 preferences.insert(QStringLiteral("encoder"), pw->encoder());
127 preferences.insert(QStringLiteral("bitrate"), pw->bitrate());
128 return preferences;
129 }
130
131 QString getEncoderPreference(const QVariantMap &preferences) const
132 {
133 QString encoder = preferences.value(QStringLiteral("encoder")).toString();
134 if (encoder.isEmpty() && !m_encoders.isEmpty()) {
135 return m_encoders.constFirst().name;
136 } else {
137 return encoder;
138 }
139 }
140
141 int getBitratePreference(const QVariantMap &preferences) const
142 {
143 int bitrate = preferences.value(QStringLiteral("bitrate")).toInt();
144 if (bitrate <= 0) {
146 } else {
147 return bitrate;
148 }
149 }
150
151private:
152 void applyPreferencesToWidget(KisAndroidMediaEncoderPreferencesWidget *pw, const QVariantMap &preferences) const
153 {
154 pw->setEncoder(getEncoderPreference(preferences));
155 pw->setBitrate(getBitratePreference(preferences));
156 }
157
158 static QString keyForFormatId(int formatId)
159 {
160 switch (formatId) {
161 case FORMAT_MP4_H264:
162 return QStringLiteral("android:mp4:h264");
163 case FORMAT_WEBM_VP8:
164 return QStringLiteral("android:webm:vp8");
165 case FORMAT_MP4_AV1:
166 return QStringLiteral("android:mp4:av1");
167 }
168 return QString();
169 }
170
171 static QString titleForFormatId(int formatId)
172 {
173 switch (formatId) {
174 case FORMAT_MP4_H264:
175 return QStringLiteral("MP4/H.264");
176 case FORMAT_WEBM_VP8:
177 return QStringLiteral("WEBM/VP8");
178 case FORMAT_MP4_AV1:
179 return QStringLiteral("MP4/AV1");
180 }
181 return QString();
182 }
183
184 static QString extensionForFormatId(int formatId)
185 {
186 switch (formatId) {
187 case FORMAT_MP4_H264:
188 case FORMAT_MP4_AV1:
189 return QStringLiteral("mp4");
190 case FORMAT_WEBM_VP8:
191 return QStringLiteral("webm");
192 }
193 return QString();
194 }
195
197 {
198 switch (formatId) {
199 case FORMAT_MP4_H264:
200 return 6000000;
201 case FORMAT_WEBM_VP8:
202 return 7000000;
203 case FORMAT_MP4_AV1:
204 return 3200000;
205 }
206 return 6000000;
207 }
208
211};
212
214{
215public:
216 explicit Context(QString *outErrorMessage = nullptr)
217 : m_outErrorMessage(outErrorMessage)
218 {
219 }
220
222 {
223 if (m_imageFormat != AV_PIX_FMT_NONE) {
224 av_freep(&m_imageBuffers[0]);
225 }
226 sws_freeContext(m_swsContext);
227 clearEncoder();
228 }
229
230 QJniEnvironment &env()
231 {
232 return m_env;
233 }
234
235 QJniObject &encoder()
236 {
237 return m_encoder;
238 }
239
240 void setEncoder(const QJniObject &encoder)
241 {
243 }
244
246 {
247 if (m_encoder.isValid()) {
248 m_encoder.callMethod<void>("cancel", "()V");
249 m_encoder = QJniObject();
250 if (m_env->ExceptionCheck()) {
251 warnFile << "JNI exception occurred cancelling encoder";
252 m_env->ExceptionDescribe();
253 m_env->ExceptionClear();
254 }
255 }
256 }
257
258 bool checkObject(const QString &title, QJniObject &obj)
259 {
260 if (checkException(title)) {
261 return true;
262 } else if (!obj.isValid()) {
263 setInternalErrorMessage(QStringLiteral("JNI object %1 invalid").arg(title));
264 return true;
265 } else {
266 return false;
267 }
268 }
269
270 bool checkResult(const QString &title, int result)
271 {
272 if (checkException(title)) {
273 return true;
274 } else if (result == STATUS_ERROR_START_ENCODER) {
275 warnFile << "Start encoder error" << result;
276 if (m_outErrorMessage) {
277 *m_outErrorMessage = i18n("Unsupported video parameters, try lowering the video FPS or size");
278 }
279 return true;
280 } else if (result == STATUS_ERROR_DRAIN_MUXER_ADD_TRACK) {
281 warnFile << "Muxer track error" << result;
282 if (m_outErrorMessage) {
283 *m_outErrorMessage = i18n("Unsupported format");
284 }
285 return true;
286 } else if (isErrorResult(result)) {
287 setInternalErrorMessage(QStringLiteral("%1 failed with code %2").arg(title).arg(result));
288 return true;
289 } else {
290 return false;
291 }
292 }
293
294 bool checkException(const QString &title)
295 {
296 if (m_env->ExceptionCheck()) {
297 setInternalErrorMessage(QStringLiteral("JNI exception in %1").arg(title));
298 m_env->ExceptionDescribe();
299 m_env->ExceptionClear();
300 return true;
301 } else {
302 return false;
303 }
304 }
305
306 SwsContext *getSwsContextFor(int inputWidth,
307 int inputHeight,
308 AVPixelFormat inputFormat,
309 int outputWidth,
310 int outputHeight,
311 AVPixelFormat outputFormat,
312 int flags)
313 {
314 return sws_getCachedContext(m_swsContext,
315 inputWidth,
316 inputHeight,
317 inputFormat,
318 outputWidth,
319 outputHeight,
320 outputFormat,
321 flags,
322 nullptr,
323 nullptr,
324 nullptr);
325 }
326
327 int imageFormat() const
328 {
329 return m_imageFormat;
330 }
331
332 uint8_t **imageBuffers()
333 {
334 return m_imageBuffers;
335 }
336
338 {
339 return m_imageLinesizes;
340 }
341
342 bool allocateImage(int outputWidth, int outputHeight, AVPixelFormat outputFormat)
343 {
344 // The Android encoder really shouldn't be changing
345 // pixel formats along the way, but just in case.
346 if (m_imageFormat != AV_PIX_FMT_NONE) {
347 m_imageFormat = AV_PIX_FMT_NONE;
348 av_freep(&m_imageBuffers[0]);
349 }
350
351 int result = av_image_alloc(m_imageBuffers, m_imageLinesizes, outputWidth, outputHeight, outputFormat, 32);
352 if (result >= 0) {
353 m_imageFormat = outputFormat;
354 return true;
355 } else {
356 setInternalErrorMessage(QStringLiteral("av_image_alloc error %1").arg(result));
357 return false;
358 }
359 }
360
361 void setInternalErrorMessage(const QString &detail)
362 {
363 warnFile << "Media encoder error:" << detail;
364 if (m_outErrorMessage) {
365 // Internal encoder errors are only really useful for developers,
366 // so there's no point in translating them.
367 *m_outErrorMessage = i18n("Internal error (%1)", detail);
368 }
369 }
370
371private:
372 static bool isErrorResult(int result)
373 {
374 return result >= 100;
375 }
376
377 QJniEnvironment m_env;
378 QJniObject m_encoder;
380 SwsContext *m_swsContext = nullptr;
381 uint8_t *m_imageBuffers[4] = {nullptr, nullptr, nullptr, nullptr};
382 int m_imageLinesizes[4] = {0, 0, 0, 0};
383 AVPixelFormat m_imageFormat = AV_PIX_FMT_NONE;
384};
385
387 QObject *parent)
388{
390 return new KisAndroidMediaEncoderRunnable(settings, parent);
391 } else {
392 return nullptr;
393 }
394}
395
397{
398 Context ctx;
399 int formatIds[] = {FORMAT_MP4_H264, FORMAT_WEBM_VP8, FORMAT_MP4_AV1};
400 for (int formatId : formatIds) {
401 checkFormatSupport(ctx, formatId, outSupportedFormats);
402 }
403}
404
410
412{
413 Format *format = static_cast<Format *>(settings().format);
414
415 QTemporaryFile tempFile;
416 QString tempFilePath;
417 if (tempFile.open()) {
418 tempFilePath = tempFile.fileName();
419 tempFile.close();
420 } else {
421 warnFile << "Failed to open temporary file:" << tempFile.errorString();
422 // Keep going, we might not actually need a temporary file.
423 }
424
425 Context ctx(&outErrorMessage);
426 int outputWidth = settings().outputSize.width();
427 int outputHeight = settings().outputSize.height();
428
429 // Set up the encoder.
430 {
431 QJniObject outputPath = QJniObject::fromString(settings().outputFile);
432 if (ctx.checkObject(QStringLiteral("outputPath"), outputPath)) {
434 }
435
436 QJniObject tempPath = QJniObject::fromString(tempFilePath);
437 if (ctx.checkObject(QStringLiteral("tempPath"), tempPath)) {
439 }
440
441 QJniObject encoderName = QJniObject::fromString(format->getEncoderPreference(settings().formatPreferences));
442 ctx.checkException(QStringLiteral("encoderName"));
443
444 ctx.setEncoder(QJniObject("org/krita/android/VideoEncoder",
445 "(IIIFLjava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V",
446 jint(format->formatId()),
447 jint(outputWidth),
448 jint(outputHeight),
449 jfloat(settings().outputFps),
450 outputPath.object<jstring>(),
451 tempPath.object<jstring>(),
452 encoderName.object<jstring>(),
453 jint(format->getBitratePreference(settings().formatPreferences))));
454 if (ctx.checkObject(QStringLiteral("encoder"), ctx.encoder())) {
456 }
457 }
458
459 if (isCancelled()) {
461 }
462
463 // Start the encoding.
464 {
465 QJniObject activity = QJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative",
466 "activity",
467 "()Landroid/app/Activity;");
468 if (ctx.checkObject(QStringLiteral("activity"), activity)) {
470 }
471
472 int startResult =
473 int(ctx.encoder().callMethod<jint>("start", "(Landroid/content/Context;)I", activity.object<jobject>()));
474 if (ctx.checkResult(QStringLiteral("start"), startResult)) {
476 }
477 }
478
479 // Encode the frames.
480 Frame frame;
481 while (nextFrame(frame)) {
482 if (isCancelled()) {
484 }
485
486 // Grab the next frame from disk.
487 QImage inputImage;
488 AVPixelFormat inputPixelFormat;
489 if (frame.readImage(inputImage)) {
490 switch (inputImage.format()) {
491 case QImage::Format_RGB32:
492 inputPixelFormat = AV_PIX_FMT_BGR0;
493 break;
494 case QImage::Format_ARGB32:
495 inputPixelFormat = AV_PIX_FMT_BGRA;
496 break;
497 default:
498 // The above are the only formats I can get the the recorder to
499 // produce, so I'm not gonna get experimental with this.
500 inputPixelFormat = AV_PIX_FMT_BGRA;
501 inputImage = inputImage.convertToFormat(QImage::Format_ARGB32);
502 if (inputImage.isNull()) {
503 warnFile << "Frame conversion from" << inputImage.format() << "failed";
504 continue;
505 }
506 break;
507 }
508 } else {
509 continue; // Keep going, some frames may be corrupted.
510 }
511
512 int instances = frame.instances();
513 for (int i = 0; i < instances; ++i) {
514 // Grab a buffer from the encoder.
515 EncodeResult prepareResult = prepare(ctx);
516 if (prepareResult != EncodeResult::Completed) {
517 return prepareResult;
518 }
519
520 // Retrieve the buffer layout.
521 EncoderImage encoderImage;
522 if (!readEncoderImage(ctx, encoderImage)) {
524 }
525
526 // Map the buffer layout to a libswscale-befitting arrangement. The
527 // layout may either have the YUV components in separate buffers or it
528 // may have the U and V components combined into a single buffer, where
529 // either U or V can come first. Which one we get depends on hardware.
530 uint8_t *dstBuffers[4] = {nullptr, nullptr, nullptr, nullptr};
531 int dstLinesizes[4] = {0, 0, 0, 0};
532 AVPixelFormat outputPixelFormat;
533 if (encoderImage.pixelStrideU == 1 && encoderImage.pixelStrideV == 1) {
534 // Separate Y, U and V buffers.
535 outputPixelFormat = AV_PIX_FMT_YUV420P;
536 dstBuffers[0] = encoderImage.bufferY;
537 dstBuffers[1] = encoderImage.bufferU;
538 dstBuffers[2] = encoderImage.bufferV;
539 dstLinesizes[0] = encoderImage.rowStrideY;
540 dstLinesizes[1] = encoderImage.rowStrideU;
541 dstLinesizes[2] = encoderImage.rowStrideV;
542
543 } else if (encoderImage.pixelStrideU == 2 && encoderImage.pixelStrideV == 2
544 && encoderImage.bufferU + 1 == encoderImage.bufferV) {
545 // One Y buffer and one combined UV buffer, U comes first.
546 outputPixelFormat = AV_PIX_FMT_NV12;
547 dstBuffers[0] = encoderImage.bufferY;
548 dstBuffers[1] = encoderImage.bufferU;
549 dstLinesizes[0] = encoderImage.rowStrideY;
550 dstLinesizes[1] = encoderImage.rowStrideU;
551
552 } else if (encoderImage.pixelStrideU == 2 && encoderImage.pixelStrideV == 2
553 && encoderImage.bufferV + 1 == encoderImage.bufferU) {
554 // One Y buffer and one combined UV buffer, V comes first.
555 outputPixelFormat = AV_PIX_FMT_NV21;
556 dstBuffers[0] = encoderImage.bufferY;
557 dstBuffers[1] = encoderImage.bufferV;
558 dstLinesizes[0] = encoderImage.rowStrideY;
559 dstLinesizes[1] = encoderImage.rowStrideV;
560
561 } else {
562 ctx.setInternalErrorMessage(QStringLiteral("unknown buffer format u%1/%2 v%3/%4")
563 .arg(quintptr(encoderImage.bufferU), 0, 16)
564 .arg(encoderImage.pixelStrideU)
565 .arg(quintptr(encoderImage.bufferV), 0, 16)
566 .arg(encoderImage.pixelStrideV));
568 }
569
570 SwsContext *swsContext = ctx.getSwsContextFor(inputImage.width(),
571 inputImage.height(),
572 inputPixelFormat,
573 outputWidth,
574 outputHeight,
575 outputPixelFormat,
576 SWS_FAST_BILINEAR);
577 if (!swsContext) {
578 ctx.setInternalErrorMessage(QStringLiteral("sws_getCachedContext"));
580 }
581
582 if (instances == 1) {
583 // Just a single frame, scale it into the native buffer.
584 const uint8_t *srcBuffers[] = {inputImage.bits(), nullptr, nullptr, nullptr};
585 const int srcLinesizes[] = {inputImage.bytesPerLine(), 0, 0, 0};
586 sws_scale(swsContext, srcBuffers, srcLinesizes, 0, inputImage.height(), dstBuffers, dstLinesizes);
587
588 } else {
589 // Repeated frame, scale it into an intermediate buffer, then
590 // copy it over to the native one for each instance.
591 if (i == 0 || ctx.imageFormat() != outputPixelFormat) {
592 if (ctx.imageFormat() != outputPixelFormat) {
593 if (!ctx.allocateImage(outputWidth, outputHeight, outputPixelFormat)) {
594 warnFile << "Encoder changed pixel format from" << int(ctx.imageFormat()) << "to"
595 << int(outputPixelFormat);
597 }
598 }
599
600 const uint8_t *srcBuffers[] = {inputImage.bits(), nullptr, nullptr, nullptr};
601 const int srcLinesizes[] = {inputImage.bytesPerLine(), 0, 0, 0};
602 sws_scale(swsContext,
603 srcBuffers,
604 srcLinesizes,
605 0,
606 inputImage.height(),
607 ctx.imageBuffers(),
608 ctx.imageLinesizes());
609 }
610
611 av_image_copy2(dstBuffers,
612 dstLinesizes,
613 ctx.imageBuffers(),
614 ctx.imageLinesizes(),
615 outputPixelFormat,
616 outputWidth,
617 outputHeight);
618 }
619
620 int commitResult = int(ctx.encoder().callMethod<jint>("commit", "()I"));
621 if (ctx.checkResult(QStringLiteral("commit"), commitResult)) {
623 }
624 }
625 }
626
627 if (isCancelled()) {
629 }
630
631 // Finish the encoder stream.
632 {
633 // Need a buffer from the encoder to tell it that it's done.
634 EncodeResult prepareResult = prepare(ctx);
635 if (prepareResult != EncodeResult::Completed) {
636 return prepareResult;
637 }
638 // Hand an empty buffer back with the end of stream flag set.
639 int finishResult = int(ctx.encoder().callMethod<jint>("finish", "()I"));
640 if (ctx.checkResult(QStringLiteral("finish"), finishResult)) {
642 }
643 }
644
645 // Drain all remaining frames out of the encoder.
646 while (true) {
647 int drainResult = drain(ctx, 1000000LL);
648 if (drainResult == DRAIN_END_OF_STREAM) {
649 break;
650 } else if (drainResult == DRAIN_ERROR) {
652 } else if (drainResult == DRAIN_CANCELLED) {
654 } else {
655 KIS_SAFE_ASSERT_RECOVER_NOOP(drainResult >= 0);
656 }
657 }
658
659 // Close the encoder, copy the temporary file to the output file if needed.
660 {
661 int closeResult = int(ctx.encoder().callMethod<jint>("close", "()I"));
662 if (ctx.checkResult(QStringLiteral("close"), closeResult)) {
664 }
665
666 if (isCancelled()) {
668 }
669
670 if (closeResult == STATUS_NEEDS_COPY) {
671 if (!copyTemporaryToOutputFile(ctx, tempFilePath, settings().outputFile)) {
673 }
674 }
675 }
676
678}
679
681{
682 while (true) {
683 if (isCancelled()) {
685 }
686
687 int prepareResult = int(ctx.encoder().callMethod<jint>("prepare", "(J)I", jlong(100000LL)));
688 if (ctx.checkResult(QStringLiteral("prepare"), prepareResult)) {
690
691 } else if (prepareResult == STATUS_TIMEOUT) {
692 int drainResult = drain(ctx, 0LL);
693 if (drainResult == DRAIN_END_OF_STREAM) {
694 ctx.setInternalErrorMessage(QStringLiteral("unexpected end of stream"));
696 } else if (drainResult == DRAIN_ERROR) {
698 } else if (drainResult == DRAIN_CANCELLED) {
700 } else {
701 KIS_SAFE_ASSERT_RECOVER_NOOP(drainResult >= 0);
702 }
703
704 } else {
705 KIS_SAFE_ASSERT_RECOVER_NOOP(prepareResult == STATUS_OK);
706 break;
707 }
708 }
710}
711
712int KisAndroidMediaEncoderRunnable::drain(Context &ctx, long long initialTimeout)
713{
714 int count = 0;
715 long long timeout = initialTimeout;
716 while (true) {
717 if (isCancelled()) {
718 return DRAIN_CANCELLED;
719 }
720
721 int drainResult = int(ctx.encoder().callMethod<jint>("drain", "(J)I", jlong(timeout)));
722
723 if (ctx.checkResult(QStringLiteral("drain"), drainResult)) {
724 return DRAIN_ERROR;
725
726 } else if (drainResult == STATUS_TIMEOUT) {
727 break;
728
729 } else if (drainResult == STATUS_END_OF_STREAM) {
730 return DRAIN_END_OF_STREAM;
731
732 } else {
734 ++count;
735 }
736 }
737 return count;
738}
739
741 const QString &tempPath,
742 const QString &outputPath)
743{
744 QFile tempFile(tempPath);
745 if (!tempFile.open(QIODevice::ReadOnly)) {
747 QStringLiteral("failed to open temp file '%1': %2").arg(tempPath).arg(tempFile.errorString()));
748 return false;
749 }
750
751 QFile outputFile(outputPath);
752 if (!outputFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
754 QStringLiteral("failed to open output file '%1': %2").arg(outputPath).arg(outputFile.errorString()));
755 return false;
756 }
757
758 QByteArray buffer;
759 buffer.resize(BUFSIZ);
760 while (true) {
761 qint64 read = tempFile.read(buffer.data(), BUFSIZ);
762 if (read < 0) {
764 QStringLiteral("failed to read from temp file '%1': %2").arg(tempPath).arg(tempFile.errorString()));
765 return false;
766 } else if (read > 0) {
767 qint64 written = outputFile.write(buffer, read);
768 if (written < 0) {
769 ctx.setInternalErrorMessage(QStringLiteral("failed to write %1 byte(s) to output file '%2': %3")
770 .arg(read)
771 .arg(outputPath)
772 .arg(outputFile.errorString()));
773 return false;
774 } else if (written != read) {
776 QStringLiteral("tried to write %1 byte(s) to output file '%2', but only wrote %3")
777 .arg(read)
778 .arg(outputPath)
779 .arg(written));
780 return false;
781 }
782 } else {
783 if (outputFile.flush()) {
784 return true;
785 } else {
786 ctx.setInternalErrorMessage(QStringLiteral("failed to flush output file '%1': %2")
787 .arg(outputPath)
788 .arg(outputFile.errorString()));
789 return false;
790 }
791 }
792 }
793}
794
796{
797 return readPlaneBuffer(ctx, 0, outImage.bufferY) && readPlaneBuffer(ctx, 1, outImage.bufferU)
798 && readPlaneBuffer(ctx, 2, outImage.bufferV) && readPlaneRowStride(ctx, 0, outImage.rowStrideY)
799 && readPlaneRowStride(ctx, 1, outImage.rowStrideU) && readPlaneRowStride(ctx, 2, outImage.rowStrideV)
800 && readPlanePixelStride(ctx, 1, outImage.pixelStrideU) && readPlanePixelStride(ctx, 2, outImage.pixelStrideV);
801}
802
803bool KisAndroidMediaEncoderRunnable::readPlaneBuffer(Context &ctx, int index, uint8_t *&outBuffer)
804{
805 QJniObject plane =
806 ctx.encoder().callObjectMethod("getInputImagePlaneBuffer", "(I)Ljava/nio/ByteBuffer;", jint(index));
807 if (ctx.checkObject(QStringLiteral("plane"), plane)) {
808 return false;
809 }
810
811 uint8_t *buffer = static_cast<uint8_t *>(ctx.env()->GetDirectBufferAddress(plane.object<jobject>()));
812 if (!buffer) {
813 ctx.setInternalErrorMessage(QStringLiteral("null plane buffer %1").arg(index));
814 return false;
815 }
816
817 outBuffer = buffer;
818 return true;
819}
820
821bool KisAndroidMediaEncoderRunnable::readPlaneRowStride(Context &ctx, int index, int &outRowStride)
822{
823 jint rowStride = ctx.encoder().callMethod<jint>("getInputImagePlaneRowStride", "(I)I", jint(index));
824 if (ctx.checkException(QStringLiteral("rowStride"))) {
825 return false;
826 } else if (rowStride <= 0) {
827 ctx.setInternalErrorMessage(QStringLiteral("invalid row stride %1: %2").arg(index).arg(rowStride));
828 return false;
829 }
830
831 outRowStride = int(rowStride);
832 return true;
833}
834
835bool KisAndroidMediaEncoderRunnable::readPlanePixelStride(Context &ctx, int index, int &outPixelStride)
836{
837 jint pixelStride = ctx.encoder().callMethod<jint>("getInputImagePlanePixelStride", "(I)I", jint(index));
838 if (ctx.checkException(QStringLiteral("pixelStride"))) {
839 return false;
840 } else if (pixelStride <= 0) {
841 ctx.setInternalErrorMessage(QStringLiteral("invalid pixel stride %1: %2").arg(index).arg(pixelStride));
842 return false;
843 }
844
845 outPixelStride = int(pixelStride);
846 return true;
847}
848
850 int formatId,
851 QVector<KisMediaEncoderFormat *> &outSupportedFormats)
852{
853 QJniObject supports = QJniObject::callStaticObjectMethod("org/krita/android/VideoEncoder",
854 "getSupportsForFormat",
855 "(I)Ljava/util/List;",
856 jint(formatId));
857 if (ctx.checkObject(QStringLiteral("supports"), supports)) {
858 return;
859 }
860
861 jint count = supports.callMethod<jint>("size", "()I");
862 if (ctx.checkException(QStringLiteral("size"))) {
863 return;
864 }
865
867 for (jint i = 0; i < count; ++i) {
868 QJniObject entry = supports.callObjectMethod("get", "(I)Ljava/lang/Object;", i);
869 if (ctx.checkObject(QStringLiteral("entry"), entry)) {
870 continue;
871 }
872
873 QJniObject name = entry.getObjectField("name", "Ljava/lang/String;");
874 if (ctx.checkObject(QStringLiteral("name"), name)) {
875 continue;
876 }
877
878 QString nameString = name.toString();
879 if (ctx.checkException(QStringLiteral("nameString")) || nameString.isEmpty()) {
880 continue;
881 }
882
883 bool hardware = entry.getField<jboolean>("hardware");
884 if (ctx.checkException(QStringLiteral("hardware"))) {
885 continue;
886 }
887
888 encoders.append({nameString, hardware});
889 }
890
891 if (!encoders.isEmpty()) {
892 outSupportedFormats.append(new Format(formatId, encoders));
893 }
894}
895
897 : QWidget(parent)
898{
899 QFormLayout *form = new QFormLayout(this);
900
901 m_cmbEncoder = new QComboBox;
902 form->addRow(i18n("Encoder:"), m_cmbEncoder);
903
904 m_intBitrate = new QSpinBox;
905 m_intBitrate->setRange(1, 999999999);
906 form->addRow(i18n("Bitrate:"), m_intBitrate);
907}
908
909void KisAndroidMediaEncoderPreferencesWidget::addEncoderOption(const QString &title, const QString &key)
910{
911 m_cmbEncoder->addItem(title, QVariant(key));
912}
913
915{
916 return m_cmbEncoder->currentData().toString();
917}
918
920{
921 int index = 0;
922 int count = m_cmbEncoder->count();
923 for (int i = 0; i < count; ++i) {
924 if (m_cmbEncoder->itemData(i).toString() == key) {
925 index = i;
926 break;
927 }
928 }
929 m_cmbEncoder->setCurrentIndex(index);
930}
931
933{
934 return m_intBitrate->value();
935}
936
void addEncoderOption(const QString &title, const QString &key)
bool checkObject(const QString &title, QJniObject &obj)
SwsContext * getSwsContextFor(int inputWidth, int inputHeight, AVPixelFormat inputFormat, int outputWidth, int outputHeight, AVPixelFormat outputFormat, int flags)
bool checkResult(const QString &title, int result)
bool allocateImage(int outputWidth, int outputHeight, AVPixelFormat outputFormat)
QWidget * createPreferencesWidget(const QVariantMap &preferences) const override
void applyPreferencesToWidget(KisAndroidMediaEncoderPreferencesWidget *pw, const QVariantMap &preferences) const
Format(int formatId, const QVector< Encoder > &encoders)
void resetPreferencesWidget(QWidget *widget) const override
QString getEncoderPreference(const QVariantMap &preferences) const
QVariantMap getPreferencesFromWidget(QWidget *widget) const override
int getBitratePreference(const QVariantMap &preferences) const
static void checkFormatSupport(Context &ctx, int formatId, QVector< KisMediaEncoderFormat * > &outSupportedFormats)
int drain(Context &ctx, long long initialTimeout)
static bool readEncoderImage(Context &ctx, EncoderImage &outImage)
bool copyTemporaryToOutputFile(Context &ctx, const QString &tempPath, const QString &outputPath)
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)
static KisAndroidMediaEncoderRunnable * create(const KisMediaEncoderWrapperSettings &settings, QObject *parent=nullptr)
static bool readPlanePixelStride(Context &ctx, int index, int &outPixelStride)
EncodeResult encode(QString &outErrorMessage) override
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:95