Skip to content

Commit

Permalink
Merge branch 'marlam:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
ThreeDeeJay authored Sep 25, 2024
2 parents fc76632 + d8f97a0 commit 952d749
Show file tree
Hide file tree
Showing 8 changed files with 326 additions and 23 deletions.
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 73 additions & 8 deletions src/bino.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "bino.hpp"
#include "log.hpp"
#include "tools.hpp"
#include "digestiblemedia.hpp"
#include "metadata.hpp"


Expand Down Expand Up @@ -229,16 +230,13 @@ 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);
// 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);
}
Expand Down Expand Up @@ -894,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
Expand All @@ -908,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);
Expand All @@ -925,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);
Expand All @@ -935,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);
Expand All @@ -944,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);
Expand All @@ -953,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;
Expand All @@ -1006,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)
Expand Down
124 changes: 124 additions & 0 deletions src/digestiblemedia.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* This file is part of Bino, a 3D video player.
*
* Copyright (C) 2024
* Martin Lambers <[email protected]>
*
* 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 <http://www.gnu.org/licenses/>.
*/

#include <QDir>
#include <QTemporaryFile>
#include <QSharedPointer>
#include <QImage>
#include <cstring>

#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<QUrl, QSharedPointer<QTemporaryFile>> 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<QTemporaryFile> 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());
}
26 changes: 26 additions & 0 deletions src/digestiblemedia.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* This file is part of Bino, a 3D video player.
*
* Copyright (C) 2024
* Martin Lambers <[email protected]>
*
* 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 <http://www.gnu.org/licenses/>.
*/

#pragma once

#include <QUrl>


QUrl digestibleMediaUrl(const QUrl& url);
Loading

0 comments on commit 952d749

Please sign in to comment.