From 3b61340213569ff066d68b82b872047efddfa280 Mon Sep 17 00:00:00 2001 From: Martin Lambers Date: Mon, 23 Sep 2024 19:37:21 +0200 Subject: [PATCH 1/3] Remove workaround for Qt bug because it can cause segfaults --- src/bino.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/bino.cpp b/src/bino.cpp index ad26fb2..e0dca22 100644 --- a/src/bino.cpp +++ b/src/bino.cpp @@ -229,13 +229,6 @@ void Bino::mediaChanged(PlaylistEntry entry) if (entry.noMedia()) { _player->stop(); } else { - // QMediaPlayer does not work with simply setting a new source via setSource(). - // Apparently we at least need to flush all buffers with setSource(QUrl()) first. - // But that triggers playbackstateChanged() events which mess up our playlist. - // To work around this problem, we use the big hammer and destroy and recreate - // the QMediaPlayer before setting the new URL. Not exactly elegant... - stopPlaylistMode(); - startPlaylistMode(); _player->setSource(entry.url); MetaData metaData; metaData.detectCached(entry.url); From f72f9d4af4f0125a957a63f6a8f945f45aa8fa97 Mon Sep 17 00:00:00 2001 From: Martin Lambers Date: Mon, 23 Sep 2024 19:50:20 +0200 Subject: [PATCH 2/3] Convert JPEG variants to something digestible before playback This works around the problem that the Qt Multimedia backends try hardware acceleration on decoding JPEGs, which fails if the JPEG does not fit in the limitations of the accelerator. This is regularly the case for photographs. Also, this allows manual interpretation of MPO files, which are not reliably supported by Qt Multimedia backends. --- CMakeLists.txt | 2 + src/bino.cpp | 7 ++- src/digestiblemedia.cpp | 124 ++++++++++++++++++++++++++++++++++++++++ src/digestiblemedia.hpp | 26 +++++++++ src/urlloader.cpp | 51 +++++++++++++++++ src/urlloader.hpp | 47 +++++++++++++++ src/videosink.cpp | 17 +----- src/videosink.hpp | 1 - 8 files changed, 259 insertions(+), 16 deletions(-) create mode 100644 src/digestiblemedia.cpp create mode 100644 src/digestiblemedia.hpp create mode 100644 src/urlloader.cpp create mode 100644 src/urlloader.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b4311b..8693f38 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,8 @@ add_executable(bino src/commandinterpreter.hpp src/commandinterpreter.cpp src/playlisteditor.hpp src/playlisteditor.cpp src/gui.hpp src/gui.cpp + src/urlloader.hpp src/urlloader.cpp + src/digestiblemedia.hpp src/digestiblemedia.cpp src/appicon.rc) qt6_add_translations(bino TS_FILES i18n/bino_de.ts i18n/bino_ka.ts i18n/bino_zh.ts) qt6_add_resources(bino "misc" PREFIX "/" FILES diff --git a/src/bino.cpp b/src/bino.cpp index e0dca22..4270401 100644 --- a/src/bino.cpp +++ b/src/bino.cpp @@ -26,6 +26,7 @@ #include "bino.hpp" #include "log.hpp" #include "tools.hpp" +#include "digestiblemedia.hpp" #include "metadata.hpp" @@ -229,9 +230,13 @@ void Bino::mediaChanged(PlaylistEntry entry) if (entry.noMedia()) { _player->stop(); } else { - _player->setSource(entry.url); + // Get meta data MetaData metaData; metaData.detectCached(entry.url); + // Special handling of files that cannot be digested by QtMultimedia directly + QUrl digestibleUrl = digestibleMediaUrl(entry.url); + // Set new source + _player->setSource(digestibleUrl); if (entry.videoTrack >= 0) { _player->setActiveVideoTrack(entry.videoTrack); } diff --git a/src/digestiblemedia.cpp b/src/digestiblemedia.cpp new file mode 100644 index 0000000..baf1ac0 --- /dev/null +++ b/src/digestiblemedia.cpp @@ -0,0 +1,124 @@ +/* + * This file is part of Bino, a 3D video player. + * + * Copyright (C) 2024 + * Martin Lambers + * + * 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, see . + */ + +#include +#include +#include +#include +#include + +#include "urlloader.hpp" +#include "log.hpp" +#include "digestiblemedia.hpp" + + +/* We convert JPEGs to temporary PPMs here, for the following reasons: + * - QtMultimedia tries to decode JPEGs with hardware acceleration, which fails + * when the image dimensions (or other properties) are not within the + * constraints of the hardware decoder, which is optimized for video. + * This happens often with both the GStreamer and FFmpeg backends. + * Unfortunately there does not seem to be a fallback to software decoding. + * - MPO files can contain multiple JPEG images. In the only relevant use case, + * they contain a left and a right JPEG with a lot of junk in between (probably + * called metadata by some standard). These files typically cannot be read + * reliably by either QtMultimedia backend. So we read both JPEGs manually + * from the MPO and stack them on top of each other (top-bottom format). + * - The destination image format is PPM because it is fast to write (no + * compression) and the media backend will not be tempted to try hardware + * accelerated decoding on PPMs. + */ + +QUrl digestibleMediaUrl(const QUrl& url) +{ + static QMap> cache; + + // check if we need conversion + bool needsConversion = url.fileName().endsWith(".jpg", Qt::CaseInsensitive) + || url.fileName().endsWith(".jpeg", Qt::CaseInsensitive) + || url.fileName().endsWith(".jps", Qt::CaseInsensitive) + || url.fileName().endsWith(".mpo", Qt::CaseInsensitive); + if (!needsConversion) { + LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1 needs no conversion").arg(url.toString()))); + return url; + } + + // check if we have it cached + QSharedPointer tempFile = nullptr; + auto it = cache.find(url); + if (it != cache.end()) { + tempFile = it.value(); + LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1 is in cache: %2").arg(url.toString()).arg(tempFile->fileName()))); + } + + // build temporary file if it is not in cache yet + if (!tempFile) { + UrlLoader loader(url); + const QByteArray& data = loader.load(); + if (data.size() == 0) { + LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1: cannot download").arg(url.toString()))); + return url; + } + + QImage img; + if (!img.loadFromData(data, "JPG")) { + LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1: cannot load JPEG").arg(url.toString()))); + return url; + } + + if (url.fileName().endsWith("mpo", Qt::CaseInsensitive)) { + unsigned char jpegMarker[4] = { 0xff, 0xd8, 0xff, 0xe1 }; + QByteArrayView jpegMarkerView(jpegMarker, 4); + qsizetype nextJpeg = data.indexOf(jpegMarkerView, 4); + if (nextJpeg <= 0) { + LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1: no second jpeg marker found").arg(url.toString()))); + } else { + QImage imgRight; + if (!imgRight.loadFromData(QByteArrayView(data.data() + nextJpeg, data.size() - nextJpeg), "JPG")) { + LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1: cannot load second jpeg").arg(url.toString()))); + } else { + if (img.format() != imgRight.format() || img.size() != imgRight.size()) { + LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1: second jpeg is incompatible").arg(url.toString()))); + } else { + QImage combinedImg(img.width(), 2 * img.height(), img.format()); + for (int i = 0; i < img.height(); i++) { + std::memcpy(combinedImg.scanLine(i), img.constScanLine(i), img.bytesPerLine()); + } + for (int i = 0; i < img.height(); i++) { + std::memcpy(combinedImg.scanLine(img.height() + i), imgRight.constScanLine(i), imgRight.bytesPerLine()); + } + img = combinedImg; + } + } + } + } + + QString tmpl = QDir::tempPath() + '/' + QString("bino-XXXXXX") + QString(".ppm"); + tempFile.reset(new QTemporaryFile(tmpl)); + if (!img.save(tempFile.get(), "PPM")) { + LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1: cannot save to %2").arg(url.toString()).arg(tempFile->fileName()))); + return url; + } + + LOG_DEBUG("%s", qPrintable(QString("digestibleMediaUrl: %1 is saved in %2").arg(url.toString()).arg(tempFile->fileName()))); + cache.insert(url, tempFile); + } + + return QUrl::fromLocalFile(tempFile->fileName()); +} diff --git a/src/digestiblemedia.hpp b/src/digestiblemedia.hpp new file mode 100644 index 0000000..3e72364 --- /dev/null +++ b/src/digestiblemedia.hpp @@ -0,0 +1,26 @@ +/* + * This file is part of Bino, a 3D video player. + * + * Copyright (C) 2024 + * Martin Lambers + * + * 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, see . + */ + +#pragma once + +#include + + +QUrl digestibleMediaUrl(const QUrl& url); diff --git a/src/urlloader.cpp b/src/urlloader.cpp new file mode 100644 index 0000000..1e4b37b --- /dev/null +++ b/src/urlloader.cpp @@ -0,0 +1,51 @@ +/* + * This file is part of Bino, a 3D video player. + * + * Copyright (C) 2024 + * Martin Lambers + * + * 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, see . + */ + +#include +#include + +#include "urlloader.hpp" + + +UrlLoader::UrlLoader(const QUrl& url) : _url(url), _done(false) +{ +} + +UrlLoader::~UrlLoader() +{ +} + +void UrlLoader::urlLoaded(QNetworkReply* reply) +{ + _data = reply->readAll(); + reply->deleteLater(); + _done = true; +} + +const QByteArray& UrlLoader::load() +{ + connect(&_netAccMgr, SIGNAL(finished(QNetworkReply*)), this, SLOT(urlLoaded(QNetworkReply*))); + QNetworkRequest request(_url); + _netAccMgr.get(request); + while (!_done) { + QGuiApplication::processEvents(); + } + return _data; +} diff --git a/src/urlloader.hpp b/src/urlloader.hpp new file mode 100644 index 0000000..0affee9 --- /dev/null +++ b/src/urlloader.hpp @@ -0,0 +1,47 @@ +/* + * This file is part of Bino, a 3D video player. + * + * Copyright (C) 2024 + * Martin Lambers + * + * 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, see . + */ + +#pragma once + +#include +#include +#include +#include + + +class UrlLoader : public QObject +{ + Q_OBJECT + +private: + QUrl _url; + QNetworkAccessManager _netAccMgr; + QByteArray _data; + bool _done; + +private slots: + void urlLoaded(QNetworkReply* reply); + +public: + UrlLoader(const QUrl& url); + virtual ~UrlLoader(); + + const QByteArray& load(); +}; diff --git a/src/videosink.cpp b/src/videosink.cpp index b1cd25d..65d6eaa 100644 --- a/src/videosink.cpp +++ b/src/videosink.cpp @@ -26,7 +26,6 @@ VideoSink::VideoSink(VideoFrame* frame, VideoFrame* extFrame, bool* frameIsNew) : frameCounter(0), - fileFormatIsMPO(false), frame(frame), extFrame(extFrame), frameIsNew(frameIsNew), @@ -42,7 +41,6 @@ VideoSink::VideoSink(VideoFrame* frame, VideoFrame* extFrame, bool* frameIsNew) void VideoSink::newUrl(const QUrl& url, InputMode im, SurroundMode sm) { frameCounter = 0; - fileFormatIsMPO = false; lastFrameWasValid = false; LOG_DEBUG("initial input mode for %s: %s", qPrintable(url.toString()), inputModeToString(im)); @@ -61,8 +59,7 @@ void VideoSink::newUrl(const QUrl& url, InputMode im, SurroundMode sm) if (extension == ".jps" || extension == ".pns") { inputMode = Input_Right_Left; } else if (extension == ".mpo") { - inputMode = Input_Alternating_LR; - fileFormatIsMPO = true; + inputMode = Input_Top_Bottom; // this was converted; see digestiblemedia } if (inputMode != Input_Unknown) LOG_DEBUG("setting input mode %s from file name extension %s", inputModeToString(inputMode), qPrintable(extension)); @@ -125,17 +122,9 @@ void VideoSink::processNewFrame(const QVideoFrame& frame) LOG_DEBUG("video sink gets invalid frame and ignores it since last frame of current media was valid"); return; } - if (frame.isValid()) + if (frame.isValid()) { + LOG_DEBUG("video sink gets a valid frame"); lastFrameWasValid = true; - - // Workaround a glitch in the MPO file format: - // Gstreamer reads three frames from such files, the first two being the left - // view and the last one being the right view. - // We therefore ignore the first frame to get a proper left-right pair. - if (fileFormatIsMPO && frameCounter == 0) { - LOG_DEBUG("video sink ignores first frame from MPO file to work around glitch"); - frameCounter++; - return; } bool updateExtFrame; diff --git a/src/videosink.hpp b/src/videosink.hpp index 97007cf..436687f 100644 --- a/src/videosink.hpp +++ b/src/videosink.hpp @@ -33,7 +33,6 @@ Q_OBJECT public: unsigned long long frameCounter; // number of frames seen for this URL - bool fileFormatIsMPO; // flag to work around MPO glitches VideoFrame* frame; // target video frame VideoFrame* extFrame; // extension to target video frame, for alternating stereo bool *frameIsNew; // flag to set when the target frame represents a new frame From d8f97a06acbdca961c571e348e4d73e78319a9ab Mon Sep 17 00:00:00 2001 From: Martin Lambers Date: Mon, 23 Sep 2024 20:50:57 +0200 Subject: [PATCH 3/3] Fix alignment problems when uploading frames to texture memory --- src/bino.cpp | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/bino.cpp b/src/bino.cpp index 4270401..659a42f 100644 --- a/src/bino.cpp +++ b/src/bino.cpp @@ -892,6 +892,19 @@ bool Bino::drawSubtitleToImage(int w, int h, const QString& string) return true; } +static int alignmentFromBytesPerLine(const void* data, int bpl) +{ + int alignment = 1; + if (uint64_t(data) % 8 == 0 && bpl % 8 == 0) + alignment = 8; + else if (uint64_t(data) % 4 == 0 && bpl % 4 == 0) + alignment = 4; + else if (uint64_t(data) % 2 == 0 && bpl % 2 == 0) + alignment = 2; + LOG_FIREHOSE("convertFrameToTexture: alignment is %d (from data %p, bpl %d)", alignment, data, bpl); + return alignment; +} + void Bino::convertFrameToTexture(const VideoFrame& frame, unsigned int frameTex) { // 1. Get the frame data into plane textures @@ -906,6 +919,9 @@ void Bino::convertFrameToTexture(const VideoFrame& frame, unsigned int frameTex) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_BLUE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ALPHA); if (frame.storage == VideoFrame::Storage_Image) { + LOG_FIREHOSE("convertFrameToTexture: format is image"); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(frame.image.constBits(), frame.image.bytesPerLine())); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.image.bytesPerLine() / 4); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, frame.image.constBits()); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, GL_GREEN); @@ -923,6 +939,9 @@ void Bino::convertFrameToTexture(const VideoFrame& frame, unsigned int frameTex) if (frame.pixelFormat == QVideoFrameFormat::Format_ARGB8888 || frame.pixelFormat == QVideoFrameFormat::Format_ARGB8888_Premultiplied || frame.pixelFormat == QVideoFrameFormat::Format_XRGB8888) { + LOG_FIREHOSE("convertFrameToTexture: format argb8888"); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0) / 4); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, planeData[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_ALPHA); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, GL_RED); @@ -933,6 +952,9 @@ void Bino::convertFrameToTexture(const VideoFrame& frame, unsigned int frameTex) } else if (frame.pixelFormat == QVideoFrameFormat::Format_BGRA8888 || frame.pixelFormat == QVideoFrameFormat::Format_BGRA8888_Premultiplied || frame.pixelFormat == QVideoFrameFormat::Format_BGRX8888) { + LOG_FIREHOSE("convertFrameToTexture: format bgra8888"); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0) / 4); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, planeData[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, GL_GREEN); @@ -942,6 +964,9 @@ void Bino::convertFrameToTexture(const VideoFrame& frame, unsigned int frameTex) planeCount = 1; } else if (frame.pixelFormat == QVideoFrameFormat::Format_ABGR8888 || frame.pixelFormat == QVideoFrameFormat::Format_XBGR8888) { + LOG_FIREHOSE("convertFrameToTexture: format abgr8888"); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0) / 4); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, planeData[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_ALPHA); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, GL_BLUE); @@ -951,51 +976,91 @@ void Bino::convertFrameToTexture(const VideoFrame& frame, unsigned int frameTex) planeCount = 1; } else if (frame.pixelFormat == QVideoFrameFormat::Format_RGBA8888 || frame.pixelFormat == QVideoFrameFormat::Format_RGBX8888) { + LOG_FIREHOSE("convertFrameToTexture: format rgba8888"); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0) / 4); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, planeData[0]); planeFormat = 1; planeCount = 1; } else if (frame.pixelFormat == QVideoFrameFormat::Format_YUV420P) { + LOG_FIREHOSE("convertFrameToTexture: format yuv420p"); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[0]); glBindTexture(GL_TEXTURE_2D, _planeTexs[1]); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[1], frame.qframe.bytesPerLine(1))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(1)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w / 2, h / 2, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[1]); glBindTexture(GL_TEXTURE_2D, _planeTexs[2]); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[2], frame.qframe.bytesPerLine(2))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(2)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w / 2, h / 2, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[2]); planeFormat = 2; planeCount = 3; } else if (frame.pixelFormat == QVideoFrameFormat::Format_YUV422P) { + LOG_FIREHOSE("convertFrameToTexture: format yuv422p"); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[0]); glBindTexture(GL_TEXTURE_2D, _planeTexs[1]); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[1], frame.qframe.bytesPerLine(1))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(1)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w / 2, h, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[1]); glBindTexture(GL_TEXTURE_2D, _planeTexs[2]); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[2], frame.qframe.bytesPerLine(2))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(2)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w / 2, h, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[2]); planeFormat = 2; planeCount = 3; } else if (frame.pixelFormat == QVideoFrameFormat::Format_YV12) { + LOG_FIREHOSE("convertFrameToTexture: format yv12"); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[0]); glBindTexture(GL_TEXTURE_2D, _planeTexs[1]); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[1], frame.qframe.bytesPerLine(1))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(1)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w / 2, h / 2, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[1]); glBindTexture(GL_TEXTURE_2D, _planeTexs[2]); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[2], frame.qframe.bytesPerLine(2))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(2)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w / 2, h / 2, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[2]); planeFormat = 3; planeCount = 3; } else if (frame.pixelFormat == QVideoFrameFormat::Format_NV12) { + LOG_FIREHOSE("convertFrameToTexture: format nv12"); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0)); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[0]); glBindTexture(GL_TEXTURE_2D, _planeTexs[1]); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[1], frame.qframe.bytesPerLine(1))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(1) / 2); glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, w / 2, h / 2, 0, GL_RG, GL_UNSIGNED_BYTE, planeData[1]); planeFormat = 4; planeCount = 2; } else if (frame.pixelFormat == QVideoFrameFormat::Format_P010 || frame.pixelFormat == QVideoFrameFormat::Format_P016) { + LOG_FIREHOSE("convertFrameToTexture: format p010/p016"); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0) / 2); glTexImage2D(GL_TEXTURE_2D, 0, GL_R16, w, h, 0, GL_RED, GL_UNSIGNED_SHORT, planeData[0]); glBindTexture(GL_TEXTURE_2D, _planeTexs[1]); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[1], frame.qframe.bytesPerLine(1))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(1) / 4); glTexImage2D(GL_TEXTURE_2D, 0, GL_RG16, w / 2, h / 2, 0, GL_RG, GL_UNSIGNED_SHORT, planeData[1]); planeFormat = 4; planeCount = 2; } else if (frame.pixelFormat == QVideoFrameFormat::Format_Y8) { + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0)); + LOG_FIREHOSE("convertFrameToTexture: format y8"); glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, planeData[0]); planeFormat = 5; planeCount = 1; } else if (frame.pixelFormat == QVideoFrameFormat::Format_Y16) { + glPixelStorei(GL_UNPACK_ALIGNMENT, alignmentFromBytesPerLine(planeData[0], frame.qframe.bytesPerLine(0))); + glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.qframe.bytesPerLine(0) / 2); + LOG_FIREHOSE("convertFrameToTexture: format y16"); glTexImage2D(GL_TEXTURE_2D, 0, GL_R16, w, h, 0, GL_RED, GL_UNSIGNED_SHORT, planeData[0]); planeFormat = 5; planeCount = 1; @@ -1004,6 +1069,8 @@ void Bino::convertFrameToTexture(const VideoFrame& frame, unsigned int frameTex) std::exit(1); } } + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); // 2. Convert plane textures into linear RGB in the frame texture glBindTexture(GL_TEXTURE_2D, frameTex); if (IsOpenGLES)