From 10354f4127cc569535f28e7f758b58c5bd18bcd6 Mon Sep 17 00:00:00 2001 From: Peng Wu Date: Thu, 12 Mar 2015 00:45:02 -0700 Subject: [PATCH 01/13] Fix WinRT Audio elements cannot play Qt resouce audio files URL argument of Windows media API SetSourceFromByteStream can not be empty. Initial proper value for playing audio stream case. Task-number: QTBUG-42263 Change-Id: If0bb44b60d517228bfe8b6cb30afeeb4a8ac62d3 Reviewed-by: Andrew Knight Reviewed-by: Yoann Lopes --- src/plugins/winrt/qwinrtmediaplayercontrol.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/winrt/qwinrtmediaplayercontrol.cpp b/src/plugins/winrt/qwinrtmediaplayercontrol.cpp index 50f76fd8..333a6d1e 100644 --- a/src/plugins/winrt/qwinrtmediaplayercontrol.cpp +++ b/src/plugins/winrt/qwinrtmediaplayercontrol.cpp @@ -752,7 +752,7 @@ void QWinRTMediaPlayerControl::setMedia(const QMediaContent &media, QIODevice *s } emit mediaChanged(media); - QString urlString; + QString urlString = media.canonicalUrl().toString(); if (!d->stream) { // If we can read the file via Qt, use the byte stream approach foreach (const QMediaResource &resource, media.resources()) { From 09afe9377d41171368c083b7cb79fd888f6d8979 Mon Sep 17 00:00:00 2001 From: Timur Pocheptsov Date: Tue, 17 Mar 2015 09:48:54 +0100 Subject: [PATCH 02/13] AVFCameraViewfinderSettings - add NV12 format Add QVideoFrame::Format_NV12 (AVFoundation has kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange and kCVPixelFormatType_420YpCbCr8BiPlanarFullRange). Report it (set it) only if it's supported by renderer's surface. Add bi-planar format support into CVPixelBufferVideoBuffer. Change-Id: Ibc1c2be056bddf5cf3b595570fc40c626ee3ccf5 Reviewed-by: Yoann Lopes --- .../camera/avfcamerarenderercontrol.mm | 45 ++++++++++- .../avfcameraviewfindersettingscontrol.h | 1 + .../avfcameraviewfindersettingscontrol.mm | 77 ++++++++++++------- 3 files changed, 94 insertions(+), 29 deletions(-) diff --git a/src/plugins/avfoundation/camera/avfcamerarenderercontrol.mm b/src/plugins/avfoundation/camera/avfcamerarenderercontrol.mm index 87bfeb82..cf13635f 100644 --- a/src/plugins/avfoundation/camera/avfcamerarenderercontrol.mm +++ b/src/plugins/avfoundation/camera/avfcamerarenderercontrol.mm @@ -32,6 +32,7 @@ ****************************************************************************/ #include "avfcameraviewfindersettingscontrol.h" +#include "private/qabstractvideobuffer_p.h" #include "avfcamerarenderercontrol.h" #include "avfcamerasession.h" #include "avfcameraservice.h" @@ -39,15 +40,17 @@ #include #include + #include QT_USE_NAMESPACE -class CVPixelBufferVideoBuffer : public QAbstractVideoBuffer +class CVPixelBufferVideoBuffer : public QAbstractPlanarVideoBuffer { + friend class CVPixelBufferVideoBufferPrivate; public: CVPixelBufferVideoBuffer(CVPixelBufferRef buffer) - : QAbstractVideoBuffer(NoHandle) + : QAbstractPlanarVideoBuffer(NoHandle) , m_buffer(buffer) , m_mode(NotMapped) { @@ -61,6 +64,42 @@ public: MapMode mapMode() const { return m_mode; } + int map(QAbstractVideoBuffer::MapMode mode, int *numBytes, int bytesPerLine[4], uchar *data[4]) + { + // We only support RGBA or NV12 (or Apple's version of NV12), + // they are either 0 planes or 2. + const size_t nPlanes = CVPixelBufferGetPlaneCount(m_buffer); + Q_ASSERT(nPlanes <= 2); + + if (!nPlanes) { + data[0] = map(mode, numBytes, bytesPerLine); + return data[0] ? 1 : 0; + } + + // For a bi-planar format we have to set the parameters correctly: + if (mode != QAbstractVideoBuffer::NotMapped && m_mode == QAbstractVideoBuffer::NotMapped) { + CVPixelBufferLockBaseAddress(m_buffer, 0); + + if (numBytes) + *numBytes = CVPixelBufferGetDataSize(m_buffer); + + if (bytesPerLine) { + // At the moment we handle only bi-planar format. + bytesPerLine[0] = CVPixelBufferGetBytesPerRowOfPlane(m_buffer, 0); + bytesPerLine[1] = CVPixelBufferGetBytesPerRowOfPlane(m_buffer, 1); + } + + if (data) { + data[0] = (uchar *)CVPixelBufferGetBaseAddressOfPlane(m_buffer, 0); + data[1] = (uchar *)CVPixelBufferGetBaseAddressOfPlane(m_buffer, 1); + } + + m_mode = mode; + } + + return nPlanes; + } + uchar *map(MapMode mode, int *numBytes, int *bytesPerLine) { if (mode != NotMapped && m_mode == NotMapped) { @@ -73,7 +112,6 @@ public: *bytesPerLine = CVPixelBufferGetBytesPerRow(m_buffer); m_mode = mode; - return (uchar*)CVPixelBufferGetBaseAddress(m_buffer); } else { return 0; @@ -93,6 +131,7 @@ private: MapMode m_mode; }; + @interface AVFCaptureFramesDelegate : NSObject { @private diff --git a/src/plugins/avfoundation/camera/avfcameraviewfindersettingscontrol.h b/src/plugins/avfoundation/camera/avfcameraviewfindersettingscontrol.h index fccc938a..cf2f512a 100644 --- a/src/plugins/avfoundation/camera/avfcameraviewfindersettingscontrol.h +++ b/src/plugins/avfoundation/camera/avfcameraviewfindersettingscontrol.h @@ -74,6 +74,7 @@ private: void setFramerate(qreal minFPS, qreal maxFPS, bool useActive); void setPixelFormat(QVideoFrame::PixelFormat newFormat); AVCaptureDeviceFormat *findBestFormatMatch(const QCameraViewfinderSettings &settings) const; + QVector viewfinderPixelFormats() const; bool convertPixelFormatIfSupported(QVideoFrame::PixelFormat format, unsigned &avfFormat) const; void applySettings(); QCameraViewfinderSettings requestedSettings() const; diff --git a/src/plugins/avfoundation/camera/avfcameraviewfindersettingscontrol.mm b/src/plugins/avfoundation/camera/avfcameraviewfindersettingscontrol.mm index 250aae9c..60df1e2e 100644 --- a/src/plugins/avfoundation/camera/avfcameraviewfindersettingscontrol.mm +++ b/src/plugins/avfoundation/camera/avfcameraviewfindersettingscontrol.mm @@ -38,6 +38,8 @@ #include "avfcameraservice.h" #include "avfcameradebug.h" +#include + #include #include #include @@ -52,28 +54,6 @@ QT_BEGIN_NAMESPACE namespace { -QVector qt_viewfinder_pixel_formats(AVCaptureVideoDataOutput *videoOutput) -{ - Q_ASSERT(videoOutput); - - QVector qtFormats; - - NSArray *pixelFormats = [videoOutput availableVideoCVPixelFormatTypes]; - for (NSObject *obj in pixelFormats) { - if (![obj isKindOfClass:[NSNumber class]]) - continue; - - NSNumber *formatAsNSNumber = static_cast(obj); - // It's actually FourCharCode (== UInt32): - const QVideoFrame::PixelFormat qtFormat(AVFCameraViewfinderSettingsControl2:: - QtPixelFormatFromCVFormat([formatAsNSNumber unsignedIntValue])); - if (qtFormat != QVideoFrame::Format_Invalid) - qtFormats << qtFormat; - } - - return qtFormats; -} - bool qt_framerates_sane(const QCameraViewfinderSettings &settings) { const qreal minFPS = settings.minimumFrameRate(); @@ -269,7 +249,8 @@ QList AVFCameraViewfinderSettingsControl2::supportedV QVector framerates; - QVector pixelFormats(qt_viewfinder_pixel_formats(m_videoOutput)); + QVector pixelFormats(viewfinderPixelFormats()); + if (!pixelFormats.size()) pixelFormats << QVideoFrame::Format_Invalid; // The default value. #if QT_MAC_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_7, __IPHONE_7_0) @@ -397,6 +378,9 @@ QVideoFrame::PixelFormat AVFCameraViewfinderSettingsControl2::QtPixelFormatFromC return QVideoFrame::Format_RGB24; case kCVPixelFormatType_24BGR: return QVideoFrame::Format_BGR24; + case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange: + case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange: + return QVideoFrame::Format_NV12; default: return QVideoFrame::Format_Invalid; } @@ -414,6 +398,9 @@ bool AVFCameraViewfinderSettingsControl2::CVPixelFormatFromQtFormat(QVideoFrame: case QVideoFrame::Format_BGRA32: conv = kCVPixelFormatType_32ARGB; break; + case QVideoFrame::Format_NV12: + conv = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange; + break; // These two formats below are not supported // by QSGVideoNodeFactory_RGB, so for now I have to // disable them. @@ -467,7 +454,37 @@ AVCaptureDeviceFormat *AVFCameraViewfinderSettingsControl2::findBestFormatMatch( return nil; } -bool AVFCameraViewfinderSettingsControl2::convertPixelFormatIfSupported(QVideoFrame::PixelFormat qtFormat, unsigned &avfFormat)const +QVector AVFCameraViewfinderSettingsControl2::viewfinderPixelFormats() const +{ + Q_ASSERT(m_videoOutput); + + QVector qtFormats; + QList filter; + + NSArray *pixelFormats = [m_videoOutput availableVideoCVPixelFormatTypes]; + const QAbstractVideoSurface *surface = m_service->videoOutput() ? m_service->videoOutput()->surface() : 0; + + if (surface) + filter = surface->supportedPixelFormats(); + + for (NSObject *obj in pixelFormats) { + if (![obj isKindOfClass:[NSNumber class]]) + continue; + + NSNumber *formatAsNSNumber = static_cast(obj); + // It's actually FourCharCode (== UInt32): + const QVideoFrame::PixelFormat qtFormat(QtPixelFormatFromCVFormat([formatAsNSNumber unsignedIntValue])); + if (qtFormat != QVideoFrame::Format_Invalid && (!surface || filter.contains(qtFormat)) + && !qtFormats.contains(qtFormat)) { // Can happen, for example, with 8BiPlanar existing in video/full range. + qtFormats << qtFormat; + } + } + + return qtFormats; +} + +bool AVFCameraViewfinderSettingsControl2::convertPixelFormatIfSupported(QVideoFrame::PixelFormat qtFormat, + unsigned &avfFormat)const { Q_ASSERT(m_videoOutput); @@ -479,17 +496,25 @@ bool AVFCameraViewfinderSettingsControl2::convertPixelFormatIfSupported(QVideoFr if (!formats || !formats.count) return false; + if (m_service->videoOutput() && m_service->videoOutput()->surface()) { + const QAbstractVideoSurface *surface = m_service->videoOutput()->surface(); + if (!surface->supportedPixelFormats().contains(qtFormat)) + return false; + } + + bool found = false; for (NSObject *obj in formats) { if (![obj isKindOfClass:[NSNumber class]]) continue; + NSNumber *nsNum = static_cast(obj); if ([nsNum unsignedIntValue] == conv) { avfFormat = conv; - return true; + found = true; } } - return false; + return found; } void AVFCameraViewfinderSettingsControl2::applySettings() From 71fc289373f5f77ea08eca1b1803b508310c03ab Mon Sep 17 00:00:00 2001 From: Timur Pocheptsov Date: Wed, 18 Mar 2015 11:36:50 +0100 Subject: [PATCH 03/13] Add NV12/NV21 support into SG videonode. Add new fragment shaders and update declarative render (video node) to support NV12/NV21 pixel format. Task-number: QTBUG-45021 Change-Id: I5d52007f0da56165752268d06efca156f7496b42 Reviewed-by: Yoann Lopes --- .../qdeclarativevideooutput_render_p.h | 4 +- ...ideonode_i420.cpp => qsgvideonode_yuv.cpp} | 215 ++++++++++++++---- ...qsgvideonode_i420.h => qsgvideonode_yuv.h} | 18 +- .../qtmultimediaquicktools.pro | 4 +- 4 files changed, 178 insertions(+), 63 deletions(-) rename src/qtmultimediaquicktools/{qsgvideonode_i420.cpp => qsgvideonode_yuv.cpp} (54%) rename src/qtmultimediaquicktools/{qsgvideonode_i420.h => qsgvideonode_yuv.h} (86%) diff --git a/src/qtmultimediaquicktools/qdeclarativevideooutput_render_p.h b/src/qtmultimediaquicktools/qdeclarativevideooutput_render_p.h index d37df394..35a8afdc 100644 --- a/src/qtmultimediaquicktools/qdeclarativevideooutput_render_p.h +++ b/src/qtmultimediaquicktools/qdeclarativevideooutput_render_p.h @@ -36,7 +36,7 @@ #define QDECLARATIVEVIDEOOUTPUT_RENDER_P_H #include "qdeclarativevideooutput_backend_p.h" -#include "qsgvideonode_i420.h" +#include "qsgvideonode_yuv.h" #include "qsgvideonode_rgb.h" #include "qsgvideonode_texture.h" @@ -86,7 +86,7 @@ private: QOpenGLContext *m_glContext; QVideoFrame m_frame; bool m_frameChanged; - QSGVideoNodeFactory_I420 m_i420Factory; + QSGVideoNodeFactory_YUV m_i420Factory; QSGVideoNodeFactory_RGB m_rgbFactory; QSGVideoNodeFactory_Texture m_textureFactory; QMutex m_frameMutex; diff --git a/src/qtmultimediaquicktools/qsgvideonode_i420.cpp b/src/qtmultimediaquicktools/qsgvideonode_yuv.cpp similarity index 54% rename from src/qtmultimediaquicktools/qsgvideonode_i420.cpp rename to src/qtmultimediaquicktools/qsgvideonode_yuv.cpp index 7c9acd30..fc40d659 100644 --- a/src/qtmultimediaquicktools/qsgvideonode_i420.cpp +++ b/src/qtmultimediaquicktools/qsgvideonode_yuv.cpp @@ -30,7 +30,7 @@ ** $QT_END_LICENSE$ ** ****************************************************************************/ -#include "qsgvideonode_i420.h" +#include "qsgvideonode_yuv.h" #include #include #include @@ -40,21 +40,23 @@ QT_BEGIN_NAMESPACE -QList QSGVideoNodeFactory_I420::supportedPixelFormats( +QList QSGVideoNodeFactory_YUV::supportedPixelFormats( QAbstractVideoBuffer::HandleType handleType) const { QList formats; - if (handleType == QAbstractVideoBuffer::NoHandle) - formats << QVideoFrame::Format_YUV420P << QVideoFrame::Format_YV12; + if (handleType == QAbstractVideoBuffer::NoHandle) { + formats << QVideoFrame::Format_YUV420P << QVideoFrame::Format_YV12 + << QVideoFrame::Format_NV12 << QVideoFrame::Format_NV21; + } return formats; } -QSGVideoNode *QSGVideoNodeFactory_I420::createNode(const QVideoSurfaceFormat &format) +QSGVideoNode *QSGVideoNodeFactory_YUV::createNode(const QVideoSurfaceFormat &format) { if (supportedPixelFormats(format.handleType()).contains(format.pixelFormat())) - return new QSGVideoNode_I420(format); + return new QSGVideoNode_YUV(format); return 0; } @@ -136,12 +138,85 @@ protected: int m_id_opacity; }; - -class QSGVideoMaterial_YUV420 : public QSGMaterial +class QSGVideoMaterialShader_NV_12_21 : public QSGVideoMaterialShader_YUV420 { public: - QSGVideoMaterial_YUV420(const QVideoSurfaceFormat &format); - ~QSGVideoMaterial_YUV420(); + QSGVideoMaterialShader_NV_12_21(bool isNV21) : m_isNV21(isNV21) { + } + + virtual void updateState(const RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial); + +protected: + + virtual const char *vertexShader() const { + const char *shader = + "uniform highp mat4 qt_Matrix; \n" + "uniform highp float yWidth; \n" + "attribute highp vec4 qt_VertexPosition; \n" + "attribute highp vec2 qt_VertexTexCoord; \n" + "varying highp vec2 yTexCoord; \n" + "void main() { \n" + " yTexCoord = qt_VertexTexCoord * vec2(yWidth, 1);\n" + " gl_Position = qt_Matrix * qt_VertexPosition; \n" + "}"; + return shader; + } + + virtual const char *fragmentShader() const { + static const char *shaderNV12 = + "uniform sampler2D yTexture; \n" + "uniform sampler2D uvTexture; \n" + "uniform mediump mat4 colorMatrix; \n" + "uniform lowp float opacity; \n" + "varying highp vec2 yTexCoord; \n" + "void main() \n" + "{ \n" + " mediump float Y = texture2D(yTexture, yTexCoord).r; \n" + " mediump vec2 UV = texture2D(uvTexture, yTexCoord).ra; \n" + " mediump vec4 color = vec4(Y, UV.x, UV.y, 1.); \n" + " gl_FragColor = colorMatrix * color * opacity; \n" + "}"; + + static const char *shaderNV21 = + "uniform sampler2D yTexture; \n" + "uniform sampler2D uvTexture; \n" + "uniform mediump mat4 colorMatrix; \n" + "uniform lowp float opacity; \n" + "varying highp vec2 yTexCoord; \n" + "void main() \n" + "{ \n" + " mediump float Y = texture2D(yTexture, yTexCoord).r; \n" + " mediump vec2 UV = texture2D(uvTexture, yTexCoord).ar; \n" + " mediump vec4 color = vec4(Y, UV.x, UV.y, 1.); \n" + " gl_FragColor = colorMatrix * color * opacity; \n" + "}"; + return m_isNV21 ? shaderNV21 : shaderNV12; + } + + virtual void initialize() { + m_id_yTexture = program()->uniformLocation("yTexture"); + m_id_uTexture = program()->uniformLocation("uvTexture"); + m_id_matrix = program()->uniformLocation("qt_Matrix"); + m_id_yWidth = program()->uniformLocation("yWidth"); + m_id_colorMatrix = program()->uniformLocation("colorMatrix"); + m_id_opacity = program()->uniformLocation("opacity"); + } + +private: + bool m_isNV21; +}; + + +class QSGVideoMaterial_YUV : public QSGMaterial +{ +public: + QSGVideoMaterial_YUV(const QVideoSurfaceFormat &format); + ~QSGVideoMaterial_YUV(); + + bool isNV12_21() const { + const QVideoFrame::PixelFormat pf = m_format.pixelFormat(); + return pf == QVideoFrame::Format_NV12 || pf == QVideoFrame::Format_NV21; + } virtual QSGMaterialType *type() const { static QSGMaterialType theType; @@ -149,18 +224,25 @@ public: } virtual QSGMaterialShader *createShader() const { + const QVideoFrame::PixelFormat pf = m_format.pixelFormat(); + if (isNV12_21()) + return new QSGVideoMaterialShader_NV_12_21(pf == QVideoFrame::Format_NV21); + return new QSGVideoMaterialShader_YUV420; } virtual int compare(const QSGMaterial *other) const { - const QSGVideoMaterial_YUV420 *m = static_cast(other); + const QSGVideoMaterial_YUV *m = static_cast(other); int d = m_textureIds[0] - m->m_textureIds[0]; if (d) return d; - else if ((d = m_textureIds[1] - m->m_textureIds[1]) != 0) + + d = m_textureIds[1] - m->m_textureIds[1]; + + if (m_textureIds.size() == 2 || d != 0) return d; - else - return m_textureIds[2] - m->m_textureIds[2]; + + return m_textureIds[2] - m->m_textureIds[2]; } void updateBlending() { @@ -173,13 +255,12 @@ public: } void bind(); - void bindTexture(int id, int w, int h, const uchar *bits); + void bindTexture(int id, int w, int h, const uchar *bits, GLenum format); QVideoSurfaceFormat m_format; QSize m_textureSize; - static const uint Num_Texture_IDs = 3; - GLuint m_textureIds[Num_Texture_IDs]; + QVector m_textureIds; qreal m_opacity; GLfloat m_yWidth; @@ -190,13 +271,13 @@ public: QMutex m_frameMutex; }; -QSGVideoMaterial_YUV420::QSGVideoMaterial_YUV420(const QVideoSurfaceFormat &format) : +QSGVideoMaterial_YUV::QSGVideoMaterial_YUV(const QVideoSurfaceFormat &format) : m_format(format), m_opacity(1.0), m_yWidth(1.0), m_uvWidth(1.0) { - memset(m_textureIds, 0, sizeof(m_textureIds)); + m_textureIds.resize(isNV12_21() ? 2 : 3); switch (format.yCbCrColorSpace()) { case QVideoSurfaceFormat::YCbCr_JPEG: @@ -225,20 +306,19 @@ QSGVideoMaterial_YUV420::QSGVideoMaterial_YUV420(const QVideoSurfaceFormat &form setFlag(Blending, false); } -QSGVideoMaterial_YUV420::~QSGVideoMaterial_YUV420() +QSGVideoMaterial_YUV::~QSGVideoMaterial_YUV() { if (!m_textureSize.isEmpty()) { if (QOpenGLContext *current = QOpenGLContext::currentContext()) - current->functions()->glDeleteTextures(Num_Texture_IDs, m_textureIds); + current->functions()->glDeleteTextures(m_textureIds.size(), &m_textureIds[0]); else - qWarning() << "QSGVideoMaterial_YUV420: Cannot obtain GL context, unable to delete textures"; + qWarning() << "QSGVideoMaterial_YUV: Cannot obtain GL context, unable to delete textures"; } } -void QSGVideoMaterial_YUV420::bind() +void QSGVideoMaterial_YUV::bind() { QOpenGLFunctions *functions = QOpenGLContext::currentContext()->functions(); - QMutexLocker lock(&m_frameMutex); if (m_frame.isValid()) { if (m_frame.map(QAbstractVideoBuffer::ReadOnly)) { @@ -248,31 +328,43 @@ void QSGVideoMaterial_YUV420::bind() // Frame has changed size, recreate textures... if (m_textureSize != m_frame.size()) { if (!m_textureSize.isEmpty()) - functions->glDeleteTextures(Num_Texture_IDs, m_textureIds); - functions->glGenTextures(Num_Texture_IDs, m_textureIds); + functions->glDeleteTextures(m_textureIds.size(), &m_textureIds[0]); + functions->glGenTextures(m_textureIds.size(), &m_textureIds[0]); m_textureSize = m_frame.size(); } - const int y = 0; - const int u = m_frame.pixelFormat() == QVideoFrame::Format_YUV420P ? 1 : 2; - const int v = m_frame.pixelFormat() == QVideoFrame::Format_YUV420P ? 2 : 1; - - m_yWidth = qreal(fw) / m_frame.bytesPerLine(y); - m_uvWidth = qreal(fw) / (2 * m_frame.bytesPerLine(u)); - GLint previousAlignment; functions->glGetIntegerv(GL_UNPACK_ALIGNMENT, &previousAlignment); functions->glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - functions->glActiveTexture(GL_TEXTURE1); - bindTexture(m_textureIds[1], m_frame.bytesPerLine(u), fh / 2, m_frame.bits(u)); - functions->glActiveTexture(GL_TEXTURE2); - bindTexture(m_textureIds[2], m_frame.bytesPerLine(v), fh / 2, m_frame.bits(v)); - functions->glActiveTexture(GL_TEXTURE0); // Finish with 0 as default texture unit - bindTexture(m_textureIds[0], m_frame.bytesPerLine(y), fh, m_frame.bits(y)); + if (isNV12_21()) { + const int y = 0; + const int uv = 1; + + m_yWidth = qreal(fw) / m_frame.bytesPerLine(y); + m_uvWidth = m_yWidth; + + functions->glActiveTexture(GL_TEXTURE1); + bindTexture(m_textureIds[1], m_frame.bytesPerLine(uv) / 2, fh / 2, m_frame.bits(uv), GL_LUMINANCE_ALPHA); + functions->glActiveTexture(GL_TEXTURE0); // Finish with 0 as default texture unit + bindTexture(m_textureIds[0], m_frame.bytesPerLine(y), fh, m_frame.bits(y), GL_LUMINANCE); + } else { + const int y = 0; + const int u = m_frame.pixelFormat() == QVideoFrame::Format_YUV420P ? 1 : 2; + const int v = m_frame.pixelFormat() == QVideoFrame::Format_YUV420P ? 2 : 1; + + m_yWidth = qreal(fw) / m_frame.bytesPerLine(y); + m_uvWidth = qreal(fw) / (2 * m_frame.bytesPerLine(u)); + + functions->glActiveTexture(GL_TEXTURE1); + bindTexture(m_textureIds[1], m_frame.bytesPerLine(u), fh / 2, m_frame.bits(u), GL_LUMINANCE); + functions->glActiveTexture(GL_TEXTURE2); + bindTexture(m_textureIds[2], m_frame.bytesPerLine(v), fh / 2, m_frame.bits(v), GL_LUMINANCE); + functions->glActiveTexture(GL_TEXTURE0); // Finish with 0 as default texture unit + bindTexture(m_textureIds[0], m_frame.bytesPerLine(y), fh, m_frame.bits(y), GL_LUMINANCE); + } functions->glPixelStorei(GL_UNPACK_ALIGNMENT, previousAlignment); - m_frame.unmap(); } @@ -280,51 +372,52 @@ void QSGVideoMaterial_YUV420::bind() } else { functions->glActiveTexture(GL_TEXTURE1); functions->glBindTexture(GL_TEXTURE_2D, m_textureIds[1]); - functions->glActiveTexture(GL_TEXTURE2); - functions->glBindTexture(GL_TEXTURE_2D, m_textureIds[2]); + if (!isNV12_21()) { + functions->glActiveTexture(GL_TEXTURE2); + functions->glBindTexture(GL_TEXTURE_2D, m_textureIds[2]); + } functions->glActiveTexture(GL_TEXTURE0); // Finish with 0 as default texture unit functions->glBindTexture(GL_TEXTURE_2D, m_textureIds[0]); } } -void QSGVideoMaterial_YUV420::bindTexture(int id, int w, int h, const uchar *bits) +void QSGVideoMaterial_YUV::bindTexture(int id, int w, int h, const uchar *bits, GLenum format) { QOpenGLFunctions *functions = QOpenGLContext::currentContext()->functions(); functions->glBindTexture(GL_TEXTURE_2D, id); - functions->glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, w, h, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, bits); + functions->glTexImage2D(GL_TEXTURE_2D, 0, format, w, h, 0, format, GL_UNSIGNED_BYTE, bits); functions->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); functions->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); functions->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); functions->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); } -QSGVideoNode_I420::QSGVideoNode_I420(const QVideoSurfaceFormat &format) : +QSGVideoNode_YUV::QSGVideoNode_YUV(const QVideoSurfaceFormat &format) : m_format(format) { setFlag(QSGNode::OwnsMaterial); - m_material = new QSGVideoMaterial_YUV420(format); + m_material = new QSGVideoMaterial_YUV(format); setMaterial(m_material); } -QSGVideoNode_I420::~QSGVideoNode_I420() +QSGVideoNode_YUV::~QSGVideoNode_YUV() { } -void QSGVideoNode_I420::setCurrentFrame(const QVideoFrame &frame, FrameFlags) +void QSGVideoNode_YUV::setCurrentFrame(const QVideoFrame &frame, FrameFlags) { m_material->setCurrentFrame(frame); markDirty(DirtyMaterial); } - void QSGVideoMaterialShader_YUV420::updateState(const RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) { Q_UNUSED(oldMaterial); - QSGVideoMaterial_YUV420 *mat = static_cast(newMaterial); + QSGVideoMaterial_YUV *mat = static_cast(newMaterial); program()->setUniformValue(m_id_yTexture, 0); program()->setUniformValue(m_id_uTexture, 1); program()->setUniformValue(m_id_vTexture, 2); @@ -342,4 +435,26 @@ void QSGVideoMaterialShader_YUV420::updateState(const RenderState &state, program()->setUniformValue(m_id_matrix, state.combinedMatrix()); } +void QSGVideoMaterialShader_NV_12_21::updateState(const RenderState &state, + QSGMaterial *newMaterial, + QSGMaterial *oldMaterial) +{ + Q_UNUSED(oldMaterial); + + QSGVideoMaterial_YUV *mat = static_cast(newMaterial); + program()->setUniformValue(m_id_yTexture, 0); + program()->setUniformValue(m_id_uTexture, 1); + + mat->bind(); + + program()->setUniformValue(m_id_colorMatrix, mat->m_colorMatrix); + program()->setUniformValue(m_id_yWidth, mat->m_yWidth); + if (state.isOpacityDirty()) { + mat->m_opacity = state.opacity(); + program()->setUniformValue(m_id_opacity, GLfloat(mat->m_opacity)); + } + if (state.isMatrixDirty()) + program()->setUniformValue(m_id_matrix, state.combinedMatrix()); +} + QT_END_NAMESPACE diff --git a/src/qtmultimediaquicktools/qsgvideonode_i420.h b/src/qtmultimediaquicktools/qsgvideonode_yuv.h similarity index 86% rename from src/qtmultimediaquicktools/qsgvideonode_i420.h rename to src/qtmultimediaquicktools/qsgvideonode_yuv.h index dd97a9ab..776f0a5a 100644 --- a/src/qtmultimediaquicktools/qsgvideonode_i420.h +++ b/src/qtmultimediaquicktools/qsgvideonode_yuv.h @@ -31,20 +31,20 @@ ** ****************************************************************************/ -#ifndef QSGVIDEONODE_I420_H -#define QSGVIDEONODE_I420_H +#ifndef QSGVIDEONODE_YUV_H +#define QSGVIDEONODE_YUV_H #include #include QT_BEGIN_NAMESPACE -class QSGVideoMaterial_YUV420; -class QSGVideoNode_I420 : public QSGVideoNode +class QSGVideoMaterial_YUV; +class QSGVideoNode_YUV : public QSGVideoNode { public: - QSGVideoNode_I420(const QVideoSurfaceFormat &format); - ~QSGVideoNode_I420(); + QSGVideoNode_YUV(const QVideoSurfaceFormat &format); + ~QSGVideoNode_YUV(); virtual QVideoFrame::PixelFormat pixelFormat() const { return m_format.pixelFormat(); @@ -58,10 +58,10 @@ private: void bindTexture(int id, int unit, int w, int h, const uchar *bits); QVideoSurfaceFormat m_format; - QSGVideoMaterial_YUV420 *m_material; + QSGVideoMaterial_YUV *m_material; }; -class QSGVideoNodeFactory_I420 : public QSGVideoNodeFactoryInterface { +class QSGVideoNodeFactory_YUV : public QSGVideoNodeFactoryInterface { public: QList supportedPixelFormats(QAbstractVideoBuffer::HandleType handleType) const; QSGVideoNode *createNode(const QVideoSurfaceFormat &format); @@ -69,4 +69,4 @@ public: QT_END_NAMESPACE -#endif // QSGVIDEONODE_I420_H +#endif // QSGVIDEONODE_YUV_H diff --git a/src/qtmultimediaquicktools/qtmultimediaquicktools.pro b/src/qtmultimediaquicktools/qtmultimediaquicktools.pro index 6fd38be8..917a5ac2 100644 --- a/src/qtmultimediaquicktools/qtmultimediaquicktools.pro +++ b/src/qtmultimediaquicktools/qtmultimediaquicktools.pro @@ -21,7 +21,7 @@ SOURCES += \ qdeclarativevideooutput.cpp \ qdeclarativevideooutput_render.cpp \ qdeclarativevideooutput_window.cpp \ - qsgvideonode_i420.cpp \ + qsgvideonode_yuv.cpp \ qsgvideonode_rgb.cpp \ qsgvideonode_texture.cpp @@ -29,6 +29,6 @@ HEADERS += \ $$PRIVATE_HEADERS \ qdeclarativevideooutput_render_p.h \ qdeclarativevideooutput_window_p.h \ - qsgvideonode_i420.h \ + qsgvideonode_yuv.h \ qsgvideonode_rgb.h \ qsgvideonode_texture.h From 08058f8483bd1a681edc9aa0d31d3d28a96349b7 Mon Sep 17 00:00:00 2001 From: Sergio Martins Date: Thu, 19 Mar 2015 21:18:44 +0000 Subject: [PATCH 04/13] Fix inconsistent overrides. [-Winconsistent-missing-override] Change-Id: Icb3e2a640c122424704a8e4b10172ecc7602845a Reviewed-by: Marc Mutz --- .../qdeclarativevideooutput_render_p.h | 16 ++++++++-------- .../qdeclarativevideooutput_window_p.h | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/qtmultimediaquicktools/qdeclarativevideooutput_render_p.h b/src/qtmultimediaquicktools/qdeclarativevideooutput_render_p.h index 35a8afdc..cb1168ee 100644 --- a/src/qtmultimediaquicktools/qdeclarativevideooutput_render_p.h +++ b/src/qtmultimediaquicktools/qdeclarativevideooutput_render_p.h @@ -57,14 +57,14 @@ public: QDeclarativeVideoRendererBackend(QDeclarativeVideoOutput *parent); ~QDeclarativeVideoRendererBackend(); - bool init(QMediaService *service); - void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &changeData); - void releaseSource(); - void releaseControl(); - QSize nativeSize() const; - void updateGeometry(); - QSGNode *updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *data); - QAbstractVideoSurface *videoSurface() const; + bool init(QMediaService *service) Q_DECL_OVERRIDE; + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &changeData) Q_DECL_OVERRIDE; + void releaseSource() Q_DECL_OVERRIDE; + void releaseControl() Q_DECL_OVERRIDE; + QSize nativeSize() const Q_DECL_OVERRIDE; + void updateGeometry() Q_DECL_OVERRIDE; + QSGNode *updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *data) Q_DECL_OVERRIDE; + QAbstractVideoSurface *videoSurface() const Q_DECL_OVERRIDE; QRectF adjustedViewport() const Q_DECL_OVERRIDE; QOpenGLContext *glContext() const; diff --git a/src/qtmultimediaquicktools/qdeclarativevideooutput_window_p.h b/src/qtmultimediaquicktools/qdeclarativevideooutput_window_p.h index 0cec2e20..446ce52c 100644 --- a/src/qtmultimediaquicktools/qdeclarativevideooutput_window_p.h +++ b/src/qtmultimediaquicktools/qdeclarativevideooutput_window_p.h @@ -46,14 +46,14 @@ public: QDeclarativeVideoWindowBackend(QDeclarativeVideoOutput *parent); ~QDeclarativeVideoWindowBackend(); - bool init(QMediaService *service); - void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &changeData); - void releaseSource(); - void releaseControl(); - QSize nativeSize() const; - void updateGeometry(); - QSGNode *updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *data); - QAbstractVideoSurface *videoSurface() const; + bool init(QMediaService *service) Q_DECL_OVERRIDE; + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &changeData) Q_DECL_OVERRIDE; + void releaseSource() Q_DECL_OVERRIDE; + void releaseControl() Q_DECL_OVERRIDE; + QSize nativeSize() const Q_DECL_OVERRIDE; + void updateGeometry() Q_DECL_OVERRIDE; + QSGNode *updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *data) Q_DECL_OVERRIDE; + QAbstractVideoSurface *videoSurface() const Q_DECL_OVERRIDE; QRectF adjustedViewport() const Q_DECL_OVERRIDE; private: From 14e80dc2d3a99608c559991072eb41446f7b7754 Mon Sep 17 00:00:00 2001 From: Sergio Martins Date: Sat, 28 Mar 2015 17:26:20 +0000 Subject: [PATCH 05/13] Fix QNX 6.6 build by using qSqrt() and qLn() spectrumanalyser.cpp: In member function 'void SpectrumAnalyserThread::calculateSpectrum(const QByteArray&, int, int)': spectrumanalyser.cpp:138:59: error: 'sqrt' was not declared in this scope Change-Id: Ib43c693d73d2342059092094cfc3f48a0f73b4bc Reviewed-by: Giuseppe D'Angelo --- examples/multimedia/spectrum/app/spectrumanalyser.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/multimedia/spectrum/app/spectrumanalyser.cpp b/examples/multimedia/spectrum/app/spectrumanalyser.cpp index e8a9f8dc..6199221d 100644 --- a/examples/multimedia/spectrum/app/spectrumanalyser.cpp +++ b/examples/multimedia/spectrum/app/spectrumanalyser.cpp @@ -135,8 +135,8 @@ void SpectrumAnalyserThread::calculateSpectrum(const QByteArray &buffer, if (i>0 && i 1.0); From 6e7a3657dd622d6dd1be6d3ea829ffb8ec69f1fa Mon Sep 17 00:00:00 2001 From: Andrew Knight Date: Mon, 30 Mar 2015 15:35:47 +0300 Subject: [PATCH 06/13] Remove stray includes Module includes slow down the build when PCH is disabled, so don't use them. Change-Id: Ic0bf0d938ef06dea9dba6897df592311230a6529 Reviewed-by: Yoann Lopes --- src/gsttools/qgstappsrc.cpp | 1 - src/multimedia/audio/qsamplecache_p.cpp | 6 +++++- src/multimedia/audio/qsoundeffect_pulse_p.cpp | 2 +- src/multimedia/playback/playlistfileparser.cpp | 2 ++ src/multimedia/playback/playlistfileparser_p.h | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/gsttools/qgstappsrc.cpp b/src/gsttools/qgstappsrc.cpp index 5057d65c..66c38499 100644 --- a/src/gsttools/qgstappsrc.cpp +++ b/src/gsttools/qgstappsrc.cpp @@ -34,7 +34,6 @@ #include #include "qgstappsrc_p.h" -#include QGstAppSrc::QGstAppSrc(QObject *parent) :QObject(parent) diff --git a/src/multimedia/audio/qsamplecache_p.cpp b/src/multimedia/audio/qsamplecache_p.cpp index fbb53ffc..8043b770 100644 --- a/src/multimedia/audio/qsamplecache_p.cpp +++ b/src/multimedia/audio/qsamplecache_p.cpp @@ -33,8 +33,12 @@ #include "qsamplecache_p.h" #include "qwavedecoder_p.h" -#include +#include +#include +#include + +#include //#define QT_SAMPLECACHE_DEBUG QT_BEGIN_NAMESPACE diff --git a/src/multimedia/audio/qsoundeffect_pulse_p.cpp b/src/multimedia/audio/qsoundeffect_pulse_p.cpp index 0a509a8b..ecc42cac 100644 --- a/src/multimedia/audio/qsoundeffect_pulse_p.cpp +++ b/src/multimedia/audio/qsoundeffect_pulse_p.cpp @@ -44,8 +44,8 @@ #include #include -#include #include +#include #include "qsoundeffect_pulse_p.h" diff --git a/src/multimedia/playback/playlistfileparser.cpp b/src/multimedia/playback/playlistfileparser.cpp index 55835b4e..a40bdd9f 100644 --- a/src/multimedia/playback/playlistfileparser.cpp +++ b/src/multimedia/playback/playlistfileparser.cpp @@ -33,7 +33,9 @@ #include "playlistfileparser_p.h" #include +#include #include +#include #include "qmediaobject_p.h" #include "qmediametadata.h" diff --git a/src/multimedia/playback/playlistfileparser_p.h b/src/multimedia/playback/playlistfileparser_p.h index 272fac68..3e28b96c 100644 --- a/src/multimedia/playback/playlistfileparser_p.h +++ b/src/multimedia/playback/playlistfileparser_p.h @@ -45,8 +45,8 @@ // We mean it. // -#include #include "qtmultimediadefs.h" +#include QT_BEGIN_NAMESPACE From f3ee857564934332da67cc51265841bd76d62b29 Mon Sep 17 00:00:00 2001 From: Yoann Lopes Date: Mon, 23 Mar 2015 12:07:47 +0100 Subject: [PATCH 07/13] GStreamer 1.0: show preroll frames. We need to implement the show_frame() function from GstVideoSink, which handles both preroll and normal frames, instead of just GstBaseSink.render(), which is called only for normal frames. This was changed for GStreamer 0.10 by 3b20608f. Change-Id: I4823a575d499cd0d6f9f4cb62e0420e070a05214 Reviewed-by: Andrew den Exter --- src/gsttools/qgstvideorenderersink.cpp | 6 ++++-- src/multimedia/gsttools_headers/qgstvideorenderersink_p.h | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/gsttools/qgstvideorenderersink.cpp b/src/gsttools/qgstvideorenderersink.cpp index ff489b77..c09d683a 100644 --- a/src/gsttools/qgstvideorenderersink.cpp +++ b/src/gsttools/qgstvideorenderersink.cpp @@ -415,12 +415,14 @@ void QGstVideoRendererSink::class_init(gpointer g_class, gpointer class_data) sink_parent_class = reinterpret_cast(g_type_class_peek_parent(g_class)); + GstVideoSinkClass *video_sink_class = reinterpret_cast(g_class); + video_sink_class->show_frame = QGstVideoRendererSink::show_frame; + GstBaseSinkClass *base_sink_class = reinterpret_cast(g_class); base_sink_class->get_caps = QGstVideoRendererSink::get_caps; base_sink_class->set_caps = QGstVideoRendererSink::set_caps; base_sink_class->propose_allocation = QGstVideoRendererSink::propose_allocation; base_sink_class->stop = QGstVideoRendererSink::stop; - base_sink_class->render = QGstVideoRendererSink::render; GstElementClass *element_class = reinterpret_cast(g_class); element_class->change_state = QGstVideoRendererSink::change_state; @@ -517,7 +519,7 @@ gboolean QGstVideoRendererSink::stop(GstBaseSink *base) return TRUE; } -GstFlowReturn QGstVideoRendererSink::render(GstBaseSink *base, GstBuffer *buffer) +GstFlowReturn QGstVideoRendererSink::show_frame(GstVideoSink *base, GstBuffer *buffer) { VO_SINK(base); return sink->delegate->render(buffer); diff --git a/src/multimedia/gsttools_headers/qgstvideorenderersink_p.h b/src/multimedia/gsttools_headers/qgstvideorenderersink_p.h index 48b14108..72beceea 100644 --- a/src/multimedia/gsttools_headers/qgstvideorenderersink_p.h +++ b/src/multimedia/gsttools_headers/qgstvideorenderersink_p.h @@ -153,7 +153,7 @@ private: static gboolean stop(GstBaseSink *sink); - static GstFlowReturn render(GstBaseSink *sink, GstBuffer *buffer); + static GstFlowReturn show_frame(GstVideoSink *sink, GstBuffer *buffer); private: QVideoSurfaceGstDelegate *delegate; From cbbcf4f3a54fe981ad9d7e649572f86bcdbaf4c6 Mon Sep 17 00:00:00 2001 From: Yoann Lopes Date: Mon, 23 Mar 2015 14:28:41 +0100 Subject: [PATCH 08/13] GStreamer: implement unlock() in QGstVideoRendererSink. There are cases where blocking operations happening in the video sink need to be unblocked, that's why GstBaseSink has an unlock() virtual function. Since our custom video sink blocks when starting and when rendering a frame (while waiting for the main thread to actually do these operations), we need to implement the unlock() function in order to unblock these operations when requested by GstBaseSink. Change-Id: I5cb19ea689e655f572729d931cefec8a4266c94e Reviewed-by: Andrew den Exter --- src/gsttools/qgstvideorenderersink.cpp | 17 ++++++++++++++ src/gsttools/qvideosurfacegstsink.cpp | 23 ++++++++++++++++++- .../qgstvideorenderersink_p.h | 3 +++ .../gsttools_headers/qvideosurfacegstsink_p.h | 4 ++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/gsttools/qgstvideorenderersink.cpp b/src/gsttools/qgstvideorenderersink.cpp index c09d683a..e4437922 100644 --- a/src/gsttools/qgstvideorenderersink.cpp +++ b/src/gsttools/qgstvideorenderersink.cpp @@ -201,6 +201,14 @@ void QVideoSurfaceGstDelegate::stop() waitForAsyncEvent(&locker, &m_setupCondition, 500); } +void QVideoSurfaceGstDelegate::unlock() +{ + QMutexLocker locker(&m_mutex); + + m_setupCondition.wakeAll(); + m_renderCondition.wakeAll(); +} + bool QVideoSurfaceGstDelegate::proposeAllocation(GstQuery *query) { QMutexLocker locker(&m_mutex); @@ -218,6 +226,7 @@ GstFlowReturn QVideoSurfaceGstDelegate::render(GstBuffer *buffer) { QMutexLocker locker(&m_mutex); + m_renderReturn = GST_FLOW_OK; m_renderBuffer = buffer; GstFlowReturn flowReturn = waitForAsyncEvent(&locker, &m_renderCondition, 300) @@ -423,6 +432,7 @@ void QGstVideoRendererSink::class_init(gpointer g_class, gpointer class_data) base_sink_class->set_caps = QGstVideoRendererSink::set_caps; base_sink_class->propose_allocation = QGstVideoRendererSink::propose_allocation; base_sink_class->stop = QGstVideoRendererSink::stop; + base_sink_class->unlock = QGstVideoRendererSink::unlock; GstElementClass *element_class = reinterpret_cast(g_class); element_class->change_state = QGstVideoRendererSink::change_state; @@ -519,6 +529,13 @@ gboolean QGstVideoRendererSink::stop(GstBaseSink *base) return TRUE; } +gboolean QGstVideoRendererSink::unlock(GstBaseSink *base) +{ + VO_SINK(base); + sink->delegate->unlock(); + return TRUE; +} + GstFlowReturn QGstVideoRendererSink::show_frame(GstVideoSink *base, GstBuffer *buffer) { VO_SINK(base); diff --git a/src/gsttools/qvideosurfacegstsink.cpp b/src/gsttools/qvideosurfacegstsink.cpp index 36644581..4a786ea1 100644 --- a/src/gsttools/qvideosurfacegstsink.cpp +++ b/src/gsttools/qvideosurfacegstsink.cpp @@ -162,6 +162,15 @@ void QVideoSurfaceGstDelegate::stop() m_started = false; } +void QVideoSurfaceGstDelegate::unlock() +{ + QMutexLocker locker(&m_mutex); + + m_startCanceled = true; + m_setupCondition.wakeAll(); + m_renderCondition.wakeAll(); +} + bool QVideoSurfaceGstDelegate::isActive() { QMutexLocker locker(&m_mutex); @@ -218,8 +227,9 @@ GstFlowReturn QVideoSurfaceGstDelegate::render(GstBuffer *buffer) void QVideoSurfaceGstDelegate::queuedStart() { + QMutexLocker locker(&m_mutex); + if (!m_startCanceled) { - QMutexLocker locker(&m_mutex); m_started = m_surface->start(m_format); m_setupCondition.wakeAll(); } @@ -238,6 +248,9 @@ void QVideoSurfaceGstDelegate::queuedRender() { QMutexLocker locker(&m_mutex); + if (!m_frame.isValid()) + return; + if (m_surface.isNull()) { qWarning() << "Rendering video frame to deleted surface, skip the frame"; m_renderReturn = GST_FLOW_OK; @@ -347,6 +360,7 @@ void QVideoSurfaceGstSink::class_init(gpointer g_class, gpointer class_data) base_sink_class->buffer_alloc = QVideoSurfaceGstSink::buffer_alloc; base_sink_class->start = QVideoSurfaceGstSink::start; base_sink_class->stop = QVideoSurfaceGstSink::stop; + base_sink_class->unlock = QVideoSurfaceGstSink::unlock; GstElementClass *element_class = reinterpret_cast(g_class); element_class->change_state = QVideoSurfaceGstSink::change_state; @@ -601,6 +615,13 @@ gboolean QVideoSurfaceGstSink::stop(GstBaseSink *base) return TRUE; } +gboolean QVideoSurfaceGstSink::unlock(GstBaseSink *base) +{ + VO_SINK(base); + sink->delegate->unlock(); + return TRUE; +} + GstFlowReturn QVideoSurfaceGstSink::show_frame(GstVideoSink *base, GstBuffer *buffer) { VO_SINK(base); diff --git a/src/multimedia/gsttools_headers/qgstvideorenderersink_p.h b/src/multimedia/gsttools_headers/qgstvideorenderersink_p.h index 72beceea..18670887 100644 --- a/src/multimedia/gsttools_headers/qgstvideorenderersink_p.h +++ b/src/multimedia/gsttools_headers/qgstvideorenderersink_p.h @@ -96,6 +96,7 @@ public: bool start(GstCaps *caps); void stop(); + void unlock(); bool proposeAllocation(GstQuery *query); GstFlowReturn render(GstBuffer *buffer); @@ -153,6 +154,8 @@ private: static gboolean stop(GstBaseSink *sink); + static gboolean unlock(GstBaseSink *sink); + static GstFlowReturn show_frame(GstVideoSink *sink, GstBuffer *buffer); private: diff --git a/src/multimedia/gsttools_headers/qvideosurfacegstsink_p.h b/src/multimedia/gsttools_headers/qvideosurfacegstsink_p.h index 0b253462..e8f61afe 100644 --- a/src/multimedia/gsttools_headers/qvideosurfacegstsink_p.h +++ b/src/multimedia/gsttools_headers/qvideosurfacegstsink_p.h @@ -88,6 +88,8 @@ public: bool start(const QVideoSurfaceFormat &format, int bytesPerLine); void stop(); + void unlock(); + bool isActive(); QGstBufferPoolInterface *pool() { return m_pool; } @@ -148,6 +150,8 @@ private: static gboolean start(GstBaseSink *sink); static gboolean stop(GstBaseSink *sink); + static gboolean unlock(GstBaseSink *sink); + static GstFlowReturn show_frame(GstVideoSink *sink, GstBuffer *buffer); private: From de700906a19a6b0b0aa465cf2767c4f8041ea88e Mon Sep 17 00:00:00 2001 From: Yoann Lopes Date: Tue, 24 Mar 2015 16:41:51 +0100 Subject: [PATCH 09/13] Don't error out when presenting empty frames in QSGVideoItemSurface. There's no good reason to do so. Backends can actually provide empty frames, for example when flushing the pipeline or after stopping playback. Change-Id: I687c12b667e31b25e91c3201f59c52a8969d8e05 Reviewed-by: Andrew den Exter --- src/qtmultimediaquicktools/qdeclarativevideooutput_render.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/qtmultimediaquicktools/qdeclarativevideooutput_render.cpp b/src/qtmultimediaquicktools/qdeclarativevideooutput_render.cpp index 7970ae14..f4efe47e 100644 --- a/src/qtmultimediaquicktools/qdeclarativevideooutput_render.cpp +++ b/src/qtmultimediaquicktools/qdeclarativevideooutput_render.cpp @@ -444,10 +444,6 @@ void QSGVideoItemSurface::stop() bool QSGVideoItemSurface::present(const QVideoFrame &frame) { - if (!frame.isValid()) { - qWarning() << Q_FUNC_INFO << "I'm getting bad frames here..."; - return false; - } m_backend->present(frame); return true; } From 7bb8b763732bebb9f73aaa2c3fc0a3e574009979 Mon Sep 17 00:00:00 2001 From: Yoann Lopes Date: Tue, 24 Mar 2015 16:50:31 +0100 Subject: [PATCH 10/13] GStreamer 1.0: fix frames being presented too many times. Presenting a frame originates from a gstreamer thread, we block there until the frame is actually presented in the main thread. The problem is that it was presented over and over again until the gstreamer thread was unblocked. Make sure a given frame is presented only once. Change-Id: I46f246740313968637add802f509ebffcc5c19b8 Reviewed-by: Andrew den Exter --- src/gsttools/qgstvideorenderersink.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/gsttools/qgstvideorenderersink.cpp b/src/gsttools/qgstvideorenderersink.cpp index e4437922..615348c1 100644 --- a/src/gsttools/qgstvideorenderersink.cpp +++ b/src/gsttools/qgstvideorenderersink.cpp @@ -300,8 +300,11 @@ bool QVideoSurfaceGstDelegate::handleEvent(QMutexLocker *locker) gst_caps_unref(startCaps); } else if (m_renderBuffer) { + GstBuffer *buffer = m_renderBuffer; + m_renderBuffer = 0; + m_renderReturn = GST_FLOW_ERROR; + if (m_activeRenderer && m_surface) { - GstBuffer *buffer = m_renderBuffer; gst_buffer_ref(buffer); locker->unlock(); @@ -312,15 +315,11 @@ bool QVideoSurfaceGstDelegate::handleEvent(QMutexLocker *locker) locker->relock(); - m_renderReturn = rendered - ? GST_FLOW_OK - : GST_FLOW_ERROR; - - m_renderCondition.wakeAll(); - } else { - m_renderReturn = GST_FLOW_ERROR; - m_renderCondition.wakeAll(); + if (rendered) + m_renderReturn = GST_FLOW_OK; } + + m_renderCondition.wakeAll(); } else { m_setupCondition.wakeAll(); From 9fccf8064dea35f324f822b30116828acc3855a9 Mon Sep 17 00:00:00 2001 From: Yoann Lopes Date: Mon, 30 Mar 2015 16:49:52 +0200 Subject: [PATCH 11/13] Fix incorrect warning in QML AudioEngine. We need to do an early return when an AudioCategory is successfully added to an AudioEngine, otherwise a warning is incorrectly shown. Change-Id: If310c694a703242aff7f1c5ae04ad3e40c3f1acd Reviewed-by: Christian Stromme --- src/imports/audioengine/qdeclarative_audioengine_p.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/imports/audioengine/qdeclarative_audioengine_p.cpp b/src/imports/audioengine/qdeclarative_audioengine_p.cpp index cd6405c1..cf0a2264 100644 --- a/src/imports/audioengine/qdeclarative_audioengine_p.cpp +++ b/src/imports/audioengine/qdeclarative_audioengine_p.cpp @@ -373,6 +373,7 @@ void QDeclarativeAudioEngine::appendFunction(QQmlListProperty *property if (category->name() == QLatin1String("default")) { engine->m_defaultCategory = category; } + return; } QDeclarativeAttenuationModel *attenModel = qobject_cast(value); From 4d17db19f895ddaa778120c346d8a6a33a710194 Mon Sep 17 00:00:00 2001 From: Friedemann Kleint Date: Wed, 1 Apr 2015 17:09:45 +0200 Subject: [PATCH 12/13] Fix debug stream operators. - Use QDebugStateSaver to restore space setting in stream operators instead of returning dbg.space() which breaks formatting on streams that already have nospace() set. - Fix some single character string constants, streamline code. Change-Id: I18ae7324b172ea801aa9b5fe56ddf6fe527fdde9 Reviewed-by: Yoann Lopes --- examples/multimedia/video/qmlvideo/trace.h | 5 +- examples/multimedia/video/qmlvideofx/trace.h | 5 +- src/multimedia/audio/qaudio.cpp | 37 ++++---- src/multimedia/audio/qaudioformat.cpp | 37 ++++---- src/multimedia/qmediatimerange.cpp | 12 ++- src/multimedia/video/qabstractvideobuffer.cpp | 24 +++-- .../video/qabstractvideosurface.cpp | 18 +++- src/multimedia/video/qvideoframe.cpp | 93 ++++++++++--------- src/multimedia/video/qvideosurfaceformat.cpp | 43 ++++----- 9 files changed, 150 insertions(+), 124 deletions(-) diff --git a/examples/multimedia/video/qmlvideo/trace.h b/examples/multimedia/video/qmlvideo/trace.h index 3bfb1d79..02ba6476 100644 --- a/examples/multimedia/video/qmlvideo/trace.h +++ b/examples/multimedia/video/qmlvideo/trace.h @@ -62,8 +62,9 @@ struct PtrWrapper template inline QDebug& operator<<(QDebug &debug, const Trace::PtrWrapper &wrapper) { - debug.nospace() << "[" << (void*)wrapper.m_ptr << "]"; - return debug.space(); + QDebugStateSaver saver(debug); + debug.nospace() << '[' << static_cast(wrapper.m_ptr) << ']'; + return debug; } template diff --git a/examples/multimedia/video/qmlvideofx/trace.h b/examples/multimedia/video/qmlvideofx/trace.h index 86e46b33..b251deb3 100644 --- a/examples/multimedia/video/qmlvideofx/trace.h +++ b/examples/multimedia/video/qmlvideofx/trace.h @@ -62,8 +62,9 @@ struct PtrWrapper template inline QDebug &operator<<(QDebug &debug, const Trace::PtrWrapper &wrapper) { - debug.nospace() << "[" << (void*)wrapper.m_ptr << "]"; - return debug.space(); + QDebugStateSaver saver(debug); + debug.nospace() << '[' << static_cast(wrapper.m_ptr) << ']'; + return debug; } #ifdef ENABLE_TRACE diff --git a/src/multimedia/audio/qaudio.cpp b/src/multimedia/audio/qaudio.cpp index d32b4efe..8b452a11 100644 --- a/src/multimedia/audio/qaudio.cpp +++ b/src/multimedia/audio/qaudio.cpp @@ -86,59 +86,62 @@ Q_CONSTRUCTOR_FUNCTION(qRegisterAudioMetaTypes) #ifndef QT_NO_DEBUG_STREAM QDebug operator<<(QDebug dbg, QAudio::Error error) { - QDebug nospace = dbg.nospace(); + QDebugStateSaver saver(dbg); + dbg.nospace(); switch (error) { case QAudio::NoError: - nospace << "NoError"; + dbg << "NoError"; break; case QAudio::OpenError: - nospace << "OpenError"; + dbg << "OpenError"; break; case QAudio::IOError: - nospace << "IOError"; + dbg << "IOError"; break; case QAudio::UnderrunError: - nospace << "UnderrunError"; + dbg << "UnderrunError"; break; case QAudio::FatalError: - nospace << "FatalError"; + dbg << "FatalError"; break; } - return nospace; + return dbg; } QDebug operator<<(QDebug dbg, QAudio::State state) { - QDebug nospace = dbg.nospace(); + QDebugStateSaver saver(dbg); + dbg.nospace(); switch (state) { case QAudio::ActiveState: - nospace << "ActiveState"; + dbg << "ActiveState"; break; case QAudio::SuspendedState: - nospace << "SuspendedState"; + dbg << "SuspendedState"; break; case QAudio::StoppedState: - nospace << "StoppedState"; + dbg << "StoppedState"; break; case QAudio::IdleState: - nospace << "IdleState"; + dbg << "IdleState"; break; } - return nospace; + return dbg; } QDebug operator<<(QDebug dbg, QAudio::Mode mode) { - QDebug nospace = dbg.nospace(); + QDebugStateSaver saver(dbg); + dbg.nospace(); switch (mode) { case QAudio::AudioInput: - nospace << "AudioInput"; + dbg << "AudioInput"; break; case QAudio::AudioOutput: - nospace << "AudioOutput"; + dbg << "AudioOutput"; break; } - return nospace; + return dbg; } #endif diff --git a/src/multimedia/audio/qaudioformat.cpp b/src/multimedia/audio/qaudioformat.cpp index 2c9aafb8..1249ea99 100644 --- a/src/multimedia/audio/qaudioformat.cpp +++ b/src/multimedia/audio/qaudioformat.cpp @@ -459,49 +459,50 @@ int QAudioFormat::bytesPerFrame() const #ifndef QT_NO_DEBUG_STREAM QDebug operator<<(QDebug dbg, QAudioFormat::Endian endian) { - QDebug nospace = dbg.nospace(); + QDebugStateSaver saver(dbg); + dbg.nospace(); switch (endian) { case QAudioFormat::BigEndian: - nospace << "BigEndian"; + dbg << "BigEndian"; break; case QAudioFormat::LittleEndian: - nospace << "LittleEndian"; + dbg << "LittleEndian"; break; } - return nospace; + return dbg; } QDebug operator<<(QDebug dbg, QAudioFormat::SampleType type) { - QDebug nospace = dbg.nospace(); + QDebugStateSaver saver(dbg); + dbg.nospace(); switch (type) { case QAudioFormat::SignedInt: - nospace << "SignedInt"; + dbg << "SignedInt"; break; case QAudioFormat::UnSignedInt: - nospace << "UnSignedInt"; + dbg << "UnSignedInt"; break; case QAudioFormat::Float: - nospace << "Float"; + dbg << "Float"; break; default: - nospace << "Unknown"; + dbg << "Unknown"; break; } - return nospace; + return dbg; } QDebug operator<<(QDebug dbg, const QAudioFormat &f) { - dbg.nospace() << "QAudioFormat(" << f.sampleRate(); - dbg.nospace() << "Hz, " << f.sampleSize(); - dbg.nospace() << "bit, channelCount=" << f.channelCount(); - dbg.nospace() << ", sampleType=" << f.sampleType(); - dbg.nospace() << ", byteOrder=" << f.byteOrder(); - dbg.nospace() << ", codec=" << f.codec(); - dbg.nospace() << ")"; + QDebugStateSaver saver(dbg); + dbg.nospace(); + dbg << "QAudioFormat(" << f.sampleRate() << "Hz, " + << f.sampleSize() << "bit, channelCount=" << f.channelCount() + << ", sampleType=" << f.sampleType() << ", byteOrder=" << f.byteOrder() + << ", codec=" << f.codec() << ')'; - return dbg.space(); + return dbg; } #endif diff --git a/src/multimedia/qmediatimerange.cpp b/src/multimedia/qmediatimerange.cpp index 13906b8a..b30ee043 100644 --- a/src/multimedia/qmediatimerange.cpp +++ b/src/multimedia/qmediatimerange.cpp @@ -705,11 +705,13 @@ QMediaTimeRange operator-(const QMediaTimeRange &r1, const QMediaTimeRange &r2) #ifndef QT_NO_DEBUG_STREAM QDebug operator<<(QDebug dbg, const QMediaTimeRange &range) { - dbg.nospace() << "QMediaTimeRange( "; - foreach (const QMediaTimeInterval &interval, range.intervals()) { - dbg.nospace() << "(" << interval.start() << ", " << interval.end() << ") "; - } - dbg.space() << ")"; + QDebugStateSaver saver(dbg); + dbg.nospace(); + dbg << "QMediaTimeRange( "; + foreach (const QMediaTimeInterval &interval, range.intervals()) + dbg << '(' << interval.start() << ", " << interval.end() << ") "; + dbg.space(); + dbg << ')'; return dbg; } #endif diff --git a/src/multimedia/video/qabstractvideobuffer.cpp b/src/multimedia/video/qabstractvideobuffer.cpp index 657e4fc8..ff29bd0b 100644 --- a/src/multimedia/video/qabstractvideobuffer.cpp +++ b/src/multimedia/video/qabstractvideobuffer.cpp @@ -350,33 +350,37 @@ uchar *QAbstractPlanarVideoBuffer::map(MapMode mode, int *numBytes, int *bytesPe #ifndef QT_NO_DEBUG_STREAM QDebug operator<<(QDebug dbg, QAbstractVideoBuffer::HandleType type) { + QDebugStateSaver saver(dbg); + dbg.nospace(); switch (type) { case QAbstractVideoBuffer::NoHandle: - return dbg.nospace() << "NoHandle"; + return dbg << "NoHandle"; case QAbstractVideoBuffer::GLTextureHandle: - return dbg.nospace() << "GLTextureHandle"; + return dbg << "GLTextureHandle"; case QAbstractVideoBuffer::XvShmImageHandle: - return dbg.nospace() << "XvShmImageHandle"; + return dbg << "XvShmImageHandle"; case QAbstractVideoBuffer::CoreImageHandle: - return dbg.nospace() << "CoreImageHandle"; + return dbg << "CoreImageHandle"; case QAbstractVideoBuffer::QPixmapHandle: - return dbg.nospace() << "QPixmapHandle"; + return dbg << "QPixmapHandle"; default: - return dbg.nospace() << QString(QLatin1String("UserHandle(%1)")).arg(int(type)).toLatin1().constData(); + return dbg << "UserHandle(" << int(type) << ')'; } } QDebug operator<<(QDebug dbg, QAbstractVideoBuffer::MapMode mode) { + QDebugStateSaver saver(dbg); + dbg.nospace(); switch (mode) { case QAbstractVideoBuffer::ReadOnly: - return dbg.nospace() << "ReadOnly"; + return dbg << "ReadOnly"; case QAbstractVideoBuffer::ReadWrite: - return dbg.nospace() << "ReadWrite"; + return dbg << "ReadWrite"; case QAbstractVideoBuffer::WriteOnly: - return dbg.nospace() << "WriteOnly"; + return dbg << "WriteOnly"; default: - return dbg.nospace() << "NotMapped"; + return dbg << "NotMapped"; } } #endif diff --git a/src/multimedia/video/qabstractvideosurface.cpp b/src/multimedia/video/qabstractvideosurface.cpp index d09c4e4c..c86d52dd 100644 --- a/src/multimedia/video/qabstractvideosurface.cpp +++ b/src/multimedia/video/qabstractvideosurface.cpp @@ -353,18 +353,26 @@ void QAbstractVideoSurface::setNativeResolution(const QSize &resolution) #ifndef QT_NO_DEBUG_STREAM QDebug operator<<(QDebug dbg, const QAbstractVideoSurface::Error& error) { + QDebugStateSaver saver(dbg); + dbg.nospace(); switch (error) { case QAbstractVideoSurface::UnsupportedFormatError: - return dbg.nospace() << "UnsupportedFormatError"; + dbg << "UnsupportedFormatError"; + break; case QAbstractVideoSurface::IncorrectFormatError: - return dbg.nospace() << "IncorrectFormatError"; + dbg << "IncorrectFormatError"; + break; case QAbstractVideoSurface::StoppedError: - return dbg.nospace() << "StoppedError"; + dbg << "StoppedError"; + break; case QAbstractVideoSurface::ResourceError: - return dbg.nospace() << "ResourceError"; + dbg << "ResourceError"; + break; default: - return dbg.nospace() << "NoError"; + dbg << "NoError"; + break; } + return dbg; } #endif diff --git a/src/multimedia/video/qvideoframe.cpp b/src/multimedia/video/qvideoframe.cpp index 95f6acb3..4e9e28a4 100644 --- a/src/multimedia/video/qvideoframe.cpp +++ b/src/multimedia/video/qvideoframe.cpp @@ -1002,90 +1002,94 @@ QImage::Format QVideoFrame::imageFormatFromPixelFormat(PixelFormat format) #ifndef QT_NO_DEBUG_STREAM QDebug operator<<(QDebug dbg, QVideoFrame::PixelFormat pf) { + QDebugStateSaver saver(dbg); + dbg.nospace(); switch (pf) { case QVideoFrame::Format_Invalid: - return dbg.nospace() << "Format_Invalid"; + return dbg << "Format_Invalid"; case QVideoFrame::Format_ARGB32: - return dbg.nospace() << "Format_ARGB32"; + return dbg << "Format_ARGB32"; case QVideoFrame::Format_ARGB32_Premultiplied: - return dbg.nospace() << "Format_ARGB32_Premultiplied"; + return dbg << "Format_ARGB32_Premultiplied"; case QVideoFrame::Format_RGB32: - return dbg.nospace() << "Format_RGB32"; + return dbg << "Format_RGB32"; case QVideoFrame::Format_RGB24: - return dbg.nospace() << "Format_RGB24"; + return dbg << "Format_RGB24"; case QVideoFrame::Format_RGB565: - return dbg.nospace() << "Format_RGB565"; + return dbg << "Format_RGB565"; case QVideoFrame::Format_RGB555: - return dbg.nospace() << "Format_RGB555"; + return dbg << "Format_RGB555"; case QVideoFrame::Format_ARGB8565_Premultiplied: - return dbg.nospace() << "Format_ARGB8565_Premultiplied"; + return dbg << "Format_ARGB8565_Premultiplied"; case QVideoFrame::Format_BGRA32: - return dbg.nospace() << "Format_BGRA32"; + return dbg << "Format_BGRA32"; case QVideoFrame::Format_BGRA32_Premultiplied: - return dbg.nospace() << "Format_BGRA32_Premultiplied"; + return dbg << "Format_BGRA32_Premultiplied"; case QVideoFrame::Format_BGR32: - return dbg.nospace() << "Format_BGR32"; + return dbg << "Format_BGR32"; case QVideoFrame::Format_BGR24: - return dbg.nospace() << "Format_BGR24"; + return dbg << "Format_BGR24"; case QVideoFrame::Format_BGR565: - return dbg.nospace() << "Format_BGR565"; + return dbg << "Format_BGR565"; case QVideoFrame::Format_BGR555: - return dbg.nospace() << "Format_BGR555"; + return dbg << "Format_BGR555"; case QVideoFrame::Format_BGRA5658_Premultiplied: - return dbg.nospace() << "Format_BGRA5658_Premultiplied"; + return dbg << "Format_BGRA5658_Premultiplied"; case QVideoFrame::Format_AYUV444: - return dbg.nospace() << "Format_AYUV444"; + return dbg << "Format_AYUV444"; case QVideoFrame::Format_AYUV444_Premultiplied: - return dbg.nospace() << "Format_AYUV444_Premultiplied"; + return dbg << "Format_AYUV444_Premultiplied"; case QVideoFrame::Format_YUV444: - return dbg.nospace() << "Format_YUV444"; + return dbg << "Format_YUV444"; case QVideoFrame::Format_YUV420P: - return dbg.nospace() << "Format_YUV420P"; + return dbg << "Format_YUV420P"; case QVideoFrame::Format_YV12: - return dbg.nospace() << "Format_YV12"; + return dbg << "Format_YV12"; case QVideoFrame::Format_UYVY: - return dbg.nospace() << "Format_UYVY"; + return dbg << "Format_UYVY"; case QVideoFrame::Format_YUYV: - return dbg.nospace() << "Format_YUYV"; + return dbg << "Format_YUYV"; case QVideoFrame::Format_NV12: - return dbg.nospace() << "Format_NV12"; + return dbg << "Format_NV12"; case QVideoFrame::Format_NV21: - return dbg.nospace() << "Format_NV21"; + return dbg << "Format_NV21"; case QVideoFrame::Format_IMC1: - return dbg.nospace() << "Format_IMC1"; + return dbg << "Format_IMC1"; case QVideoFrame::Format_IMC2: - return dbg.nospace() << "Format_IMC2"; + return dbg << "Format_IMC2"; case QVideoFrame::Format_IMC3: - return dbg.nospace() << "Format_IMC3"; + return dbg << "Format_IMC3"; case QVideoFrame::Format_IMC4: - return dbg.nospace() << "Format_IMC4"; + return dbg << "Format_IMC4"; case QVideoFrame::Format_Y8: - return dbg.nospace() << "Format_Y8"; + return dbg << "Format_Y8"; case QVideoFrame::Format_Y16: - return dbg.nospace() << "Format_Y16"; + return dbg << "Format_Y16"; case QVideoFrame::Format_Jpeg: - return dbg.nospace() << "Format_Jpeg"; + return dbg << "Format_Jpeg"; case QVideoFrame::Format_AdobeDng: - return dbg.nospace() << "Format_AdobeDng"; + return dbg << "Format_AdobeDng"; case QVideoFrame::Format_CameraRaw: - return dbg.nospace() << "Format_CameraRaw"; + return dbg << "Format_CameraRaw"; default: - return dbg.nospace() << QString(QLatin1String("UserType(%1)" )).arg(int(pf)).toLatin1().constData(); + return dbg << QString(QLatin1String("UserType(%1)" )).arg(int(pf)).toLatin1().constData(); } } QDebug operator<<(QDebug dbg, QVideoFrame::FieldType f) { + QDebugStateSaver saver(dbg); + dbg.nospace(); switch (f) { case QVideoFrame::TopField: - return dbg.nospace() << "TopField"; + return dbg << "TopField"; case QVideoFrame::BottomField: - return dbg.nospace() << "BottomField"; + return dbg << "BottomField"; case QVideoFrame::InterlacedFrame: - return dbg.nospace() << "InterlacedFrame"; + return dbg << "InterlacedFrame"; default: - return dbg.nospace() << "ProgressiveFrame"; + return dbg << "ProgressiveFrame"; } } @@ -1161,16 +1165,17 @@ static QString qFormatTimeStamps(qint64 start, qint64 end) QDebug operator<<(QDebug dbg, const QVideoFrame& f) { - dbg.nospace() << "QVideoFrame(" << f.size() << ", " + QDebugStateSaver saver(dbg); + dbg.nospace(); + dbg << "QVideoFrame(" << f.size() << ", " << f.pixelFormat() << ", " << f.handleType() << ", " << f.mapMode() << ", " << qFormatTimeStamps(f.startTime(), f.endTime()).toLatin1().constData(); - if (f.availableMetaData().count()) { - dbg.nospace() << ", metaData: "; - dbg.nospace() << f.availableMetaData(); - } - return dbg.nospace() << ")"; + if (f.availableMetaData().count()) + dbg << ", metaData: " << f.availableMetaData(); + dbg << ')'; + return dbg; } #endif diff --git a/src/multimedia/video/qvideosurfaceformat.cpp b/src/multimedia/video/qvideosurfaceformat.cpp index 1361dbc7..4c616b89 100644 --- a/src/multimedia/video/qvideosurfaceformat.cpp +++ b/src/multimedia/video/qvideosurfaceformat.cpp @@ -569,61 +569,62 @@ void QVideoSurfaceFormat::setProperty(const char *name, const QVariant &value) #ifndef QT_NO_DEBUG_STREAM QDebug operator<<(QDebug dbg, QVideoSurfaceFormat::YCbCrColorSpace cs) { - QDebug nospace = dbg.nospace(); + QDebugStateSaver saver(dbg); + dbg.nospace(); switch (cs) { case QVideoSurfaceFormat::YCbCr_BT601: - nospace << "YCbCr_BT601"; + dbg << "YCbCr_BT601"; break; case QVideoSurfaceFormat::YCbCr_BT709: - nospace << "YCbCr_BT709"; + dbg << "YCbCr_BT709"; break; case QVideoSurfaceFormat::YCbCr_JPEG: - nospace << "YCbCr_JPEG"; + dbg << "YCbCr_JPEG"; break; case QVideoSurfaceFormat::YCbCr_xvYCC601: - nospace << "YCbCr_xvYCC601"; + dbg << "YCbCr_xvYCC601"; break; case QVideoSurfaceFormat::YCbCr_xvYCC709: - nospace << "YCbCr_xvYCC709"; + dbg << "YCbCr_xvYCC709"; break; case QVideoSurfaceFormat::YCbCr_CustomMatrix: - nospace << "YCbCr_CustomMatrix"; + dbg << "YCbCr_CustomMatrix"; break; default: - nospace << "YCbCr_Undefined"; + dbg << "YCbCr_Undefined"; break; } - return nospace; + return dbg; } QDebug operator<<(QDebug dbg, QVideoSurfaceFormat::Direction dir) { - QDebug nospace = dbg.nospace(); + QDebugStateSaver saver(dbg); + dbg.nospace(); switch (dir) { case QVideoSurfaceFormat::BottomToTop: - nospace << "BottomToTop"; + dbg << "BottomToTop"; break; case QVideoSurfaceFormat::TopToBottom: - nospace << "TopToBottom"; + dbg << "TopToBottom"; break; } - return nospace; + return dbg; } QDebug operator<<(QDebug dbg, const QVideoSurfaceFormat &f) { - dbg.nospace() << "QVideoSurfaceFormat(" << f.pixelFormat(); - dbg.nospace() << ", " << f.frameSize(); - dbg.nospace() << ", viewport=" << f.viewport(); - dbg.nospace() << ", pixelAspectRatio=" << f.pixelAspectRatio(); - dbg.nospace() << ", handleType=" << f.handleType(); - dbg.nospace() << ", yCbCrColorSpace=" << f.yCbCrColorSpace(); - dbg.nospace() << ")"; + QDebugStateSaver saver(dbg); + dbg.nospace(); + dbg << "QVideoSurfaceFormat(" << f.pixelFormat() << ", " << f.frameSize() + << ", viewport=" << f.viewport() << ", pixelAspectRatio=" << f.pixelAspectRatio() + << ", handleType=" << f.handleType() << ", yCbCrColorSpace=" << f.yCbCrColorSpace() + << ')'; foreach(const QByteArray& propertyName, f.propertyNames()) dbg << "\n " << propertyName.data() << " = " << f.property(propertyName.data()); - return dbg.space(); + return dbg; } #endif From 63cff37741dca71f3db45ee06bc5bb06488c51f4 Mon Sep 17 00:00:00 2001 From: Yoann Lopes Date: Wed, 18 Feb 2015 16:58:02 +0100 Subject: [PATCH 13/13] QMediaPlayer: handle resource files in a cross-platform way. It was the backend's responsibility to handle resource files in an appropriate way. In practice, it was either not handled at all, or implemented in an almost identical manner in every backend that does handle it. This is now dealt with in QMediaPlayer, always passing to the backend something it will be able to play. If the backend has the StreamPlayback capability, we pass a QFile from which it streams the data. If it doesn't, we copy the resource to a temporary file and pass its path to the backend. Task-number: QTBUG-36175 Task-number: QTBUG-42263 Task-number: QTBUG-43839 Change-Id: I57b355c72692d02661baeaf74e66581ca0a0bd1d Reviewed-by: Andrew Knight Reviewed-by: Peng Wu Reviewed-by: Christian Stromme --- .../controls/qmediaplayercontrol.cpp | 5 + src/multimedia/playback/qmediaplayer.cpp | 157 ++++++++++++++---- src/multimedia/playback/qmediaplayer.h | 1 + src/multimedia/qmediaserviceprovider.cpp | 52 +++++- src/multimedia/qmediaserviceprovider_p.h | 2 + .../qandroidmediaplayercontrol.cpp | 18 +- .../mediaplayer/qandroidmediaplayercontrol.h | 3 - .../src/mediaplayer/qandroidmediaservice.cpp | 4 +- .../qandroidmetadatareadercontrol.cpp | 10 +- .../qandroidmetadatareadercontrol.h | 4 +- .../jni/androidmediametadataretriever.cpp | 9 +- .../jni/androidmediametadataretriever.h | 2 +- .../directshow/player/directshowiosource.cpp | 18 -- .../directshow/player/directshowiosource.h | 11 -- .../player/directshowplayerservice.cpp | 9 - .../mediaplayer/qgstreamerplayercontrol.cpp | 26 --- .../mediaplayer/qgstreamerplayercontrol.h | 1 - .../mmrenderermediaplayercontrol.cpp | 22 +-- .../mmrenderermediaplayercontrol.h | 1 - tests/auto/unit/qmediaplayer/qmediaplayer.pro | 1 + tests/auto/unit/qmediaplayer/testdata.qrc | 5 + .../unit/qmediaplayer/testdata/nokia-tune.mp3 | Bin 0 -> 62715 bytes .../unit/qmediaplayer/tst_qmediaplayer.cpp | 90 +++++++++- .../mockmediaplayercontrol.h | 7 +- .../mockmediaserviceprovider.h | 11 ++ 25 files changed, 304 insertions(+), 165 deletions(-) create mode 100644 tests/auto/unit/qmediaplayer/testdata.qrc create mode 100644 tests/auto/unit/qmediaplayer/testdata/nokia-tune.mp3 diff --git a/src/multimedia/controls/qmediaplayercontrol.cpp b/src/multimedia/controls/qmediaplayercontrol.cpp index 1eccb762..9ea6fde8 100644 --- a/src/multimedia/controls/qmediaplayercontrol.cpp +++ b/src/multimedia/controls/qmediaplayercontrol.cpp @@ -315,6 +315,11 @@ QMediaPlayerControl::QMediaPlayerControl(QObject *parent): Setting the media to a null QMediaContent will cause the control to discard all information relating to the current media source and to cease all I/O operations related to that media. + + Qt resource files are never passed as is. If the service supports + QMediaServiceProviderHint::StreamPlayback, a \a stream is supplied, pointing to an opened + QFile. Otherwise, the resource is copied into a temporary file and \a media contains the + url to that file. */ /*! diff --git a/src/multimedia/playback/qmediaplayer.cpp b/src/multimedia/playback/qmediaplayer.cpp index aae4c7ef..b43faa2b 100644 --- a/src/multimedia/playback/qmediaplayer.cpp +++ b/src/multimedia/playback/qmediaplayer.cpp @@ -48,6 +48,8 @@ #include #include #include +#include +#include QT_BEGIN_NAMESPACE @@ -103,22 +105,30 @@ public: : provider(0) , control(0) , state(QMediaPlayer::StoppedState) + , status(QMediaPlayer::UnknownMediaStatus) , error(QMediaPlayer::NoError) + , ignoreNextStatusChange(-1) , playlist(0) , networkAccessControl(0) + , hasStreamPlaybackFeature(false) , nestedPlaylists(0) {} QMediaServiceProvider *provider; QMediaPlayerControl* control; QMediaPlayer::State state; + QMediaPlayer::MediaStatus status; QMediaPlayer::Error error; QString errorString; + int ignoreNextStatusChange; QPointer videoOutput; QMediaPlaylist *playlist; QMediaNetworkAccessControl *networkAccessControl; QVideoSurfaceOutput surfaceOutput; + bool hasStreamPlaybackFeature; + QMediaContent qrcMedia; + QScopedPointer qrcFile; QMediaContent rootMedia; QMediaContent pendingPlaylist; @@ -126,6 +136,8 @@ public: bool isInChain(QUrl url); int nestedPlaylists; + void setMedia(const QMediaContent &media, QIODevice *stream = 0); + void setPlaylist(QMediaPlaylist *playlist); void setPlaylistMedia(); void loadPlaylist(); @@ -137,6 +149,7 @@ public: void _q_error(int error, const QString &errorString); void _q_updateMedia(const QMediaContent&); void _q_playlistDestroyed(); + void _q_handleMediaChanged(const QMediaContent&); void _q_handlePlaylistLoaded(); void _q_handlePlaylistLoadFailed(); }; @@ -196,22 +209,30 @@ void QMediaPlayerPrivate::_q_stateChanged(QMediaPlayer::State ps) } } -void QMediaPlayerPrivate::_q_mediaStatusChanged(QMediaPlayer::MediaStatus status) +void QMediaPlayerPrivate::_q_mediaStatusChanged(QMediaPlayer::MediaStatus s) { Q_Q(QMediaPlayer); - switch (status) { - case QMediaPlayer::StalledMedia: - case QMediaPlayer::BufferingMedia: - q->addPropertyWatch("bufferStatus"); - emit q->mediaStatusChanged(status); - break; - default: - q->removePropertyWatch("bufferStatus"); - emit q->mediaStatusChanged(status); - break; + if (int(s) == ignoreNextStatusChange) { + ignoreNextStatusChange = -1; + return; } + if (s != status) { + status = s; + + switch (s) { + case QMediaPlayer::StalledMedia: + case QMediaPlayer::BufferingMedia: + q->addPropertyWatch("bufferStatus"); + break; + default: + q->removePropertyWatch("bufferStatus"); + break; + } + + emit q->mediaStatusChanged(s); + } } void QMediaPlayerPrivate::_q_error(int error, const QString &errorString) @@ -276,7 +297,7 @@ void QMediaPlayerPrivate::_q_updateMedia(const QMediaContent &media) const QMediaPlayer::State currentState = state; - control->setMedia(media, 0); + setMedia(media, 0); if (!media.isNull()) { switch (currentState) { @@ -297,11 +318,76 @@ void QMediaPlayerPrivate::_q_updateMedia(const QMediaContent &media) void QMediaPlayerPrivate::_q_playlistDestroyed() { playlist = 0; + setMedia(QMediaContent(), 0); +} + +void QMediaPlayerPrivate::setMedia(const QMediaContent &media, QIODevice *stream) +{ + Q_Q(QMediaPlayer); if (!control) return; - control->setMedia(QMediaContent(), 0); + QScopedPointer file; + + // Backends can't play qrc files directly. + // If the backend supports StreamPlayback, we pass a QFile for that resource. + // If it doesn't, we copy the data to a temporary file and pass its path. + if (!media.isNull() && !stream && media.canonicalUrl().scheme() == QLatin1String("qrc")) { + qrcMedia = media; + + file.reset(new QFile(QLatin1Char(':') + media.canonicalUrl().path())); + if (!file->open(QFile::ReadOnly)) { + QMetaObject::invokeMethod(q, "_q_error", Qt::QueuedConnection, + Q_ARG(int, QMediaPlayer::ResourceError), + Q_ARG(QString, QObject::tr("Attempting to play invalid Qt resource"))); + QMetaObject::invokeMethod(q, "_q_mediaStatusChanged", Qt::QueuedConnection, + Q_ARG(QMediaPlayer::MediaStatus, QMediaPlayer::InvalidMedia)); + file.reset(); + // Ignore the next NoMedia status change, we just want to clear the current media + // on the backend side since we can't load the new one and we want to be in the + // InvalidMedia status. + ignoreNextStatusChange = QMediaPlayer::NoMedia; + control->setMedia(QMediaContent(), 0); + + } else if (hasStreamPlaybackFeature) { + control->setMedia(media, file.data()); + } else { + QTemporaryFile *tempFile = new QTemporaryFile; + + // Preserve original file extension, some backends might not load the file if it doesn't + // have an extension. + const QString suffix = QFileInfo(*file).suffix(); + if (!suffix.isEmpty()) + tempFile->setFileTemplate(tempFile->fileTemplate() + QLatin1Char('.') + suffix); + + // Copy the qrc data into the temporary file + tempFile->open(); + char buffer[4096]; + while (true) { + qint64 len = file->read(buffer, sizeof(buffer)); + if (len < 1) + break; + tempFile->write(buffer, len); + } + tempFile->close(); + + file.reset(tempFile); + control->setMedia(QMediaContent(QUrl::fromLocalFile(file->fileName())), 0); + } + } else { + qrcMedia = QMediaContent(); + control->setMedia(media, stream); + } + + qrcFile.swap(file); // Cleans up any previous file +} + +void QMediaPlayerPrivate::_q_handleMediaChanged(const QMediaContent &media) +{ + Q_Q(QMediaPlayer); + + emit q->currentMediaChanged(qrcMedia.isNull() ? media : qrcMedia); } void QMediaPlayerPrivate::setPlaylist(QMediaPlaylist *pls) @@ -333,7 +419,7 @@ void QMediaPlayerPrivate::setPlaylistMedia() playlist->next(); } return; - } else if (control != 0) { + } else { // If we've just switched to a new playlist, // then last emitted currentMediaChanged was a playlist. // Make sure we emit currentMediaChanged if new playlist has @@ -344,14 +430,14 @@ void QMediaPlayerPrivate::setPlaylistMedia() // test.wav -- processed by backend, // media is not changed, // frontend needs to emit currentMediaChanged - bool isSameMedia = (control->media() == playlist->currentMedia()); - control->setMedia(playlist->currentMedia(), 0); + bool isSameMedia = (q->currentMedia() == playlist->currentMedia()); + setMedia(playlist->currentMedia(), 0); if (isSameMedia) { - emit q->currentMediaChanged(control->media()); + emit q->currentMediaChanged(q->currentMedia()); } } } else { - q->setMedia(QMediaContent(), 0); + setMedia(QMediaContent(), 0); } } @@ -441,7 +527,7 @@ void QMediaPlayerPrivate::_q_handlePlaylistLoadFailed() if (playlist) playlist->next(); else - control->setMedia(QMediaContent(), 0); + setMedia(QMediaContent(), 0); } static QMediaService *playerService(QMediaPlayer::Flags flags) @@ -484,7 +570,7 @@ QMediaPlayer::QMediaPlayer(QObject *parent, QMediaPlayer::Flags flags): d->control = qobject_cast(d->service->requestControl(QMediaPlayerControl_iid)); d->networkAccessControl = qobject_cast(d->service->requestControl(QMediaNetworkAccessControl_iid)); if (d->control != 0) { - connect(d->control, SIGNAL(mediaChanged(QMediaContent)), SIGNAL(currentMediaChanged(QMediaContent))); + connect(d->control, SIGNAL(mediaChanged(QMediaContent)), SLOT(_q_handleMediaChanged(QMediaContent))); connect(d->control, SIGNAL(stateChanged(QMediaPlayer::State)), SLOT(_q_stateChanged(QMediaPlayer::State))); connect(d->control, SIGNAL(mediaStatusChanged(QMediaPlayer::MediaStatus)), SLOT(_q_mediaStatusChanged(QMediaPlayer::MediaStatus))); @@ -500,11 +586,16 @@ QMediaPlayer::QMediaPlayer(QObject *parent, QMediaPlayer::Flags flags): connect(d->control, SIGNAL(playbackRateChanged(qreal)), SIGNAL(playbackRateChanged(qreal))); connect(d->control, SIGNAL(bufferStatusChanged(int)), SIGNAL(bufferStatusChanged(int))); - if (d->control->state() == PlayingState) + d->state = d->control->state(); + d->status = d->control->mediaStatus(); + + if (d->state == PlayingState) addPropertyWatch("position"); - if (d->control->mediaStatus() == StalledMedia || d->control->mediaStatus() == BufferingMedia) + if (d->status == StalledMedia || d->status == BufferingMedia) addPropertyWatch("bufferStatus"); + + d->hasStreamPlaybackFeature = d->provider->supportedFeatures(d->service).testFlag(QMediaServiceProviderHint::StreamPlayback); } if (d->networkAccessControl != 0) { connect(d->networkAccessControl, SIGNAL(configurationChanged(QNetworkConfiguration)), @@ -549,7 +640,9 @@ const QIODevice *QMediaPlayer::mediaStream() const { Q_D(const QMediaPlayer); - if (d->control != 0) + // When playing a resource file, we might have passed a QFile to the backend. Hide it from + // the user. + if (d->control && d->qrcMedia.isNull()) return d->control->mediaStream(); return 0; @@ -566,7 +659,12 @@ QMediaContent QMediaPlayer::currentMedia() const { Q_D(const QMediaPlayer); - if (d->control != 0) + // When playing a resource file, don't return the backend's current media, which + // can be a temporary file. + if (!d->qrcMedia.isNull()) + return d->qrcMedia; + + if (d->control) return d->control->media(); return QMediaContent(); @@ -600,12 +698,7 @@ QMediaPlayer::State QMediaPlayer::state() const QMediaPlayer::MediaStatus QMediaPlayer::mediaStatus() const { - Q_D(const QMediaPlayer); - - if (d->control != 0) - return d->control->mediaStatus(); - - return QMediaPlayer::UnknownMediaStatus; + return d_func()->status; } qint64 QMediaPlayer::duration() const @@ -877,8 +970,8 @@ void QMediaPlayer::setMedia(const QMediaContent &media, QIODevice *stream) // reset playlist to the 1st item media.playlist()->setCurrentIndex(0); d->setPlaylist(media.playlist()); - } else if (d->control != 0) { - d->control->setMedia(media, stream); + } else { + d->setMedia(media, stream); } } diff --git a/src/multimedia/playback/qmediaplayer.h b/src/multimedia/playback/qmediaplayer.h index b1215eee..735f1113 100644 --- a/src/multimedia/playback/qmediaplayer.h +++ b/src/multimedia/playback/qmediaplayer.h @@ -202,6 +202,7 @@ private: Q_PRIVATE_SLOT(d_func(), void _q_error(int, const QString &)) Q_PRIVATE_SLOT(d_func(), void _q_updateMedia(const QMediaContent&)) Q_PRIVATE_SLOT(d_func(), void _q_playlistDestroyed()) + Q_PRIVATE_SLOT(d_func(), void _q_handleMediaChanged(const QMediaContent&)) Q_PRIVATE_SLOT(d_func(), void _q_handlePlaylistLoaded()) Q_PRIVATE_SLOT(d_func(), void _q_handlePlaylistLoadFailed()) }; diff --git a/src/multimedia/qmediaserviceprovider.cpp b/src/multimedia/qmediaserviceprovider.cpp index 563af846..658679c5 100644 --- a/src/multimedia/qmediaserviceprovider.cpp +++ b/src/multimedia/qmediaserviceprovider.cpp @@ -299,7 +299,14 @@ Q_GLOBAL_STATIC_WITH_ARGS(QMediaPluginLoader, loader, class QPluginServiceProvider : public QMediaServiceProvider { - QMap pluginMap; + struct MediaServiceData { + QByteArray type; + QMediaServiceProviderPlugin *plugin; + + MediaServiceData() : plugin(0) { } + }; + + QMap mediaServiceData; public: QMediaService* requestService(const QByteArray &type, const QMediaServiceProviderHint &hint) @@ -416,8 +423,12 @@ public: if (plugin != 0) { QMediaService *service = plugin->create(key); - if (service != 0) - pluginMap.insert(service, plugin); + if (service != 0) { + MediaServiceData d; + d.type = type; + d.plugin = plugin; + mediaServiceData.insert(service, d); + } return service; } @@ -430,13 +441,30 @@ public: void releaseService(QMediaService *service) { if (service != 0) { - QMediaServiceProviderPlugin *plugin = pluginMap.take(service); + MediaServiceData d = mediaServiceData.take(service); - if (plugin != 0) - plugin->release(service); + if (d.plugin != 0) + d.plugin->release(service); } } + QMediaServiceProviderHint::Features supportedFeatures(const QMediaService *service) const + { + if (service) { + MediaServiceData d = mediaServiceData.value(service); + + if (d.plugin) { + QMediaServiceFeaturesInterface *iface = + qobject_cast(d.plugin); + + if (iface) + return iface->supportedFeatures(d.type); + } + } + + return QMediaServiceProviderHint::Features(); + } + QMultimedia::SupportEstimate hasSupport(const QByteArray &serviceType, const QString &mimeType, const QStringList& codecs, @@ -660,6 +688,18 @@ Q_GLOBAL_STATIC(QPluginServiceProvider, pluginProvider); Releases a media \a service requested with requestService(). */ +/*! + \fn QMediaServiceProvider::supportedFeatures(const QMediaService *service) const + + Returns the features supported by a given \a service. +*/ +QMediaServiceProviderHint::Features QMediaServiceProvider::supportedFeatures(const QMediaService *service) const +{ + Q_UNUSED(service); + + return QMediaServiceProviderHint::Features(0); +} + /*! \fn QMultimedia::SupportEstimate QMediaServiceProvider::hasSupport(const QByteArray &serviceType, const QString &mimeType, const QStringList& codecs, int flags) const diff --git a/src/multimedia/qmediaserviceprovider_p.h b/src/multimedia/qmediaserviceprovider_p.h index 62ee510c..4230c427 100644 --- a/src/multimedia/qmediaserviceprovider_p.h +++ b/src/multimedia/qmediaserviceprovider_p.h @@ -53,6 +53,8 @@ public: virtual QMediaService* requestService(const QByteArray &type, const QMediaServiceProviderHint &hint = QMediaServiceProviderHint()) = 0; virtual void releaseService(QMediaService *service) = 0; + virtual QMediaServiceProviderHint::Features supportedFeatures(const QMediaService *service) const; + virtual QMultimedia::SupportEstimate hasSupport(const QByteArray &serviceType, const QString &mimeType, const QStringList& codecs, diff --git a/src/plugins/android/src/mediaplayer/qandroidmediaplayercontrol.cpp b/src/plugins/android/src/mediaplayer/qandroidmediaplayercontrol.cpp index c65dec44..9a050e7a 100644 --- a/src/plugins/android/src/mediaplayer/qandroidmediaplayercontrol.cpp +++ b/src/plugins/android/src/mediaplayer/qandroidmediaplayercontrol.cpp @@ -318,8 +318,6 @@ void QAndroidMediaPlayerControl::setMedia(const QMediaContent &mediaContent, if ((mState & (AndroidMediaPlayer::Idle | AndroidMediaPlayer::Uninitialized)) == 0) mMediaPlayer->release(); - QString mediaPath; - if (mediaContent.isNull()) { setMediaStatus(QMediaPlayer::NoMedia); } else { @@ -330,29 +328,17 @@ void QAndroidMediaPlayerControl::setMedia(const QMediaContent &mediaContent, return; } - const QUrl url = mediaContent.canonicalUrl(); - if (url.scheme() == QLatin1String("qrc")) { - const QString path = url.toString().mid(3); - mTempFile.reset(QTemporaryFile::createNativeFile(path)); - if (!mTempFile.isNull()) - mediaPath = QStringLiteral("file://") + mTempFile->fileName(); - } else { - mediaPath = url.toString(QUrl::FullyEncoded); - } - if (mVideoSize.isValid() && mVideoOutput) mVideoOutput->setVideoSize(mVideoSize); if ((mMediaPlayer->display() == 0) && mVideoOutput) mMediaPlayer->setDisplay(mVideoOutput->surfaceTexture()); - mMediaPlayer->setDataSource(mediaPath); + mMediaPlayer->setDataSource(mediaContent.canonicalUrl().toString(QUrl::FullyEncoded)); mMediaPlayer->prepareAsync(); } - if (!mReloadingMedia) { + if (!mReloadingMedia) Q_EMIT mediaChanged(mMediaContent); - Q_EMIT actualMediaLocationChanged(mediaPath); - } resetBufferingProgress(); diff --git a/src/plugins/android/src/mediaplayer/qandroidmediaplayercontrol.h b/src/plugins/android/src/mediaplayer/qandroidmediaplayercontrol.h index dfc3853a..3f92d809 100644 --- a/src/plugins/android/src/mediaplayer/qandroidmediaplayercontrol.h +++ b/src/plugins/android/src/mediaplayer/qandroidmediaplayercontrol.h @@ -37,7 +37,6 @@ #include #include #include -#include QT_BEGIN_NAMESPACE @@ -72,7 +71,6 @@ public: Q_SIGNALS: void metaDataUpdated(); - void actualMediaLocationChanged(const QString &url); public Q_SLOTS: void setPosition(qint64 position) Q_DECL_OVERRIDE; @@ -112,7 +110,6 @@ private: int mPendingVolume; int mPendingMute; bool mReloadingMedia; - QScopedPointer mTempFile; int mActiveStateChangeNotifiers; void setState(QMediaPlayer::State state); diff --git a/src/plugins/android/src/mediaplayer/qandroidmediaservice.cpp b/src/plugins/android/src/mediaplayer/qandroidmediaservice.cpp index 94df8d3c..74943ca6 100644 --- a/src/plugins/android/src/mediaplayer/qandroidmediaservice.cpp +++ b/src/plugins/android/src/mediaplayer/qandroidmediaservice.cpp @@ -45,8 +45,8 @@ QAndroidMediaService::QAndroidMediaService(QObject *parent) { mMediaControl = new QAndroidMediaPlayerControl; mMetadataControl = new QAndroidMetaDataReaderControl; - connect(mMediaControl, SIGNAL(actualMediaLocationChanged(QString)), - mMetadataControl, SLOT(onMediaChanged(QString))); + connect(mMediaControl, SIGNAL(mediaChanged(QMediaContent)), + mMetadataControl, SLOT(onMediaChanged(QMediaContent))); connect(mMediaControl, SIGNAL(metaDataUpdated()), mMetadataControl, SLOT(onUpdateMetaData())); } diff --git a/src/plugins/android/src/mediaplayer/qandroidmetadatareadercontrol.cpp b/src/plugins/android/src/mediaplayer/qandroidmetadatareadercontrol.cpp index 81d7cf1a..d09a7734 100644 --- a/src/plugins/android/src/mediaplayer/qandroidmetadatareadercontrol.cpp +++ b/src/plugins/android/src/mediaplayer/qandroidmetadatareadercontrol.cpp @@ -93,18 +93,18 @@ QStringList QAndroidMetaDataReaderControl::availableMetaData() const return m_metadata.keys(); } -void QAndroidMetaDataReaderControl::onMediaChanged(const QString &url) +void QAndroidMetaDataReaderControl::onMediaChanged(const QMediaContent &media) { if (!m_retriever) return; - m_mediaLocation = url; + m_mediaContent = media; updateData(); } void QAndroidMetaDataReaderControl::onUpdateMetaData() { - if (!m_retriever || m_mediaLocation.isEmpty()) + if (!m_retriever || m_mediaContent.isNull()) return; updateData(); @@ -114,8 +114,8 @@ void QAndroidMetaDataReaderControl::updateData() { m_metadata.clear(); - if (!m_mediaLocation.isEmpty()) { - if (m_retriever->setDataSource(m_mediaLocation)) { + if (!m_mediaContent.isNull()) { + if (m_retriever->setDataSource(m_mediaContent.canonicalUrl())) { QString mimeType = m_retriever->extractMetadata(AndroidMediaMetadataRetriever::MimeType); if (!mimeType.isNull()) m_metadata.insert(QMediaMetaData::MediaType, mimeType); diff --git a/src/plugins/android/src/mediaplayer/qandroidmetadatareadercontrol.h b/src/plugins/android/src/mediaplayer/qandroidmetadatareadercontrol.h index 26847730..14fb01ea 100644 --- a/src/plugins/android/src/mediaplayer/qandroidmetadatareadercontrol.h +++ b/src/plugins/android/src/mediaplayer/qandroidmetadatareadercontrol.h @@ -54,13 +54,13 @@ public: QStringList availableMetaData() const Q_DECL_OVERRIDE; public Q_SLOTS: - void onMediaChanged(const QString &url); + void onMediaChanged(const QMediaContent &media); void onUpdateMetaData(); private: void updateData(); - QString m_mediaLocation; + QMediaContent m_mediaContent; bool m_available; QVariantMap m_metadata; diff --git a/src/plugins/android/src/wrappers/jni/androidmediametadataretriever.cpp b/src/plugins/android/src/wrappers/jni/androidmediametadataretriever.cpp index 9714654a..56ac0e0a 100644 --- a/src/plugins/android/src/wrappers/jni/androidmediametadataretriever.cpp +++ b/src/plugins/android/src/wrappers/jni/androidmediametadataretriever.cpp @@ -83,15 +83,14 @@ void AndroidMediaMetadataRetriever::release() m_metadataRetriever.callMethod("release"); } -bool AndroidMediaMetadataRetriever::setDataSource(const QString &urlString) +bool AndroidMediaMetadataRetriever::setDataSource(const QUrl &url) { if (!m_metadataRetriever.isValid()) return false; QJNIEnvironmentPrivate env; - QUrl url(urlString); - if (url.isLocalFile()) { // also includes qrc files (copied to a temp file) + if (url.isLocalFile()) { // also includes qrc files (copied to a temp file by QMediaPlayer) QJNIObjectPrivate string = QJNIObjectPrivate::fromString(url.path()); QJNIObjectPrivate fileInputStream("java/io/FileInputStream", "(Ljava/lang/String;)V", @@ -153,7 +152,7 @@ bool AndroidMediaMetadataRetriever::setDataSource(const QString &urlString) return false; } else if (QtAndroidPrivate::androidSdkVersion() >= 14) { // On API levels >= 14, only setDataSource(String, Map) accepts remote media - QJNIObjectPrivate string = QJNIObjectPrivate::fromString(urlString); + QJNIObjectPrivate string = QJNIObjectPrivate::fromString(url.toString(QUrl::FullyEncoded)); QJNIObjectPrivate hash("java/util/HashMap"); m_metadataRetriever.callMethod("setDataSource", @@ -165,7 +164,7 @@ bool AndroidMediaMetadataRetriever::setDataSource(const QString &urlString) } else { // While on API levels < 14, only setDataSource(Context, Uri) is available and works for // remote media... - QJNIObjectPrivate string = QJNIObjectPrivate::fromString(urlString); + QJNIObjectPrivate string = QJNIObjectPrivate::fromString(url.toString(QUrl::FullyEncoded)); QJNIObjectPrivate uri = m_metadataRetriever.callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", diff --git a/src/plugins/android/src/wrappers/jni/androidmediametadataretriever.h b/src/plugins/android/src/wrappers/jni/androidmediametadataretriever.h index 13cab774..01a98490 100644 --- a/src/plugins/android/src/wrappers/jni/androidmediametadataretriever.h +++ b/src/plugins/android/src/wrappers/jni/androidmediametadataretriever.h @@ -72,7 +72,7 @@ public: QString extractMetadata(MetadataKey key); void release(); - bool setDataSource(const QString &url); + bool setDataSource(const QUrl &url); private: QJNIObjectPrivate m_metadataRetriever; diff --git a/src/plugins/directshow/player/directshowiosource.cpp b/src/plugins/directshow/player/directshowiosource.cpp index 03b53c44..3a4e1075 100644 --- a/src/plugins/directshow/player/directshowiosource.cpp +++ b/src/plugins/directshow/player/directshowiosource.cpp @@ -605,21 +605,3 @@ HRESULT DirectShowIOSource::QueryDirection(PIN_DIRECTION *pPinDir) return S_OK; } } - -DirectShowRcSource::DirectShowRcSource(DirectShowEventLoop *loop) - : DirectShowIOSource(loop) -{ -} - -bool DirectShowRcSource::open(const QUrl &url) -{ - m_file.moveToThread(QCoreApplication::instance()->thread()); - m_file.setFileName(QLatin1Char(':') + url.path()); - - if (m_file.open(QIODevice::ReadOnly)) { - setDevice(&m_file); - return true; - } else { - return false; - } -} diff --git a/src/plugins/directshow/player/directshowiosource.h b/src/plugins/directshow/player/directshowiosource.h index 241c86fb..fb3774af 100644 --- a/src/plugins/directshow/player/directshowiosource.h +++ b/src/plugins/directshow/player/directshowiosource.h @@ -127,15 +127,4 @@ private: QMutex m_mutex; }; -class DirectShowRcSource : public DirectShowIOSource -{ -public: - DirectShowRcSource(DirectShowEventLoop *loop); - - bool open(const QUrl &url); - -private: - QFile m_file; -}; - #endif diff --git a/src/plugins/directshow/player/directshowplayerservice.cpp b/src/plugins/directshow/player/directshowplayerservice.cpp index 809839c6..67aea6e9 100644 --- a/src/plugins/directshow/player/directshowplayerservice.cpp +++ b/src/plugins/directshow/player/directshowplayerservice.cpp @@ -289,15 +289,6 @@ void DirectShowPlayerService::doSetUrlSource(QMutexLocker *locker) fileSource->Release(); locker->relock(); } - } else if (m_url.scheme() == QLatin1String("qrc")) { - DirectShowRcSource *rcSource = new DirectShowRcSource(m_loop); - - locker->unlock(); - if (rcSource->open(m_url) && SUCCEEDED(hr = m_graph->AddFilter(rcSource, L"Source"))) - source = rcSource; - else - rcSource->Release(); - locker->relock(); } if (!SUCCEEDED(hr)) { diff --git a/src/plugins/gstreamer/mediaplayer/qgstreamerplayercontrol.cpp b/src/plugins/gstreamer/mediaplayer/qgstreamerplayercontrol.cpp index 8fc301a3..4846353a 100644 --- a/src/plugins/gstreamer/mediaplayer/qgstreamerplayercontrol.cpp +++ b/src/plugins/gstreamer/mediaplayer/qgstreamerplayercontrol.cpp @@ -54,7 +54,6 @@ QT_BEGIN_NAMESPACE QGstreamerPlayerControl::QGstreamerPlayerControl(QGstreamerPlayerSession *session, QObject *parent) : QMediaPlayerControl(parent) - , m_ownStream(false) , m_session(session) , m_userRequestedState(QMediaPlayer::StoppedState) , m_currentState(QMediaPlayer::StoppedState) @@ -370,31 +369,6 @@ void QGstreamerPlayerControl::setMedia(const QMediaContent &content, QIODevice * emit bufferStatusChanged(0); } - if (m_stream && m_stream != stream) { - if (m_ownStream) - delete m_stream; - m_stream = 0; - m_ownStream = false; - } - - // If the canonical URL refers to a Qt resource, open with QFile and use - // the stream playback capability to play. - if (stream == 0 && content.canonicalUrl().scheme() == QLatin1String("qrc")) { - stream = new QFile(QLatin1Char(':') + content.canonicalUrl().path(), this); - if (!stream->open(QIODevice::ReadOnly)) { - delete stream; - m_mediaStatus = QMediaPlayer::InvalidMedia; - m_currentResource = content; - emit mediaChanged(m_currentResource); - emit error(QMediaPlayer::FormatError, tr("Attempting to play invalid Qt resource")); - if (m_currentState != QMediaPlayer::PlayingState) - m_resources->release(); - popAndNotifyState(); - return; - } - m_ownStream = true; - } - m_currentResource = content; m_stream = stream; diff --git a/src/plugins/gstreamer/mediaplayer/qgstreamerplayercontrol.h b/src/plugins/gstreamer/mediaplayer/qgstreamerplayercontrol.h index 21688506..c9621b79 100644 --- a/src/plugins/gstreamer/mediaplayer/qgstreamerplayercontrol.h +++ b/src/plugins/gstreamer/mediaplayer/qgstreamerplayercontrol.h @@ -116,7 +116,6 @@ private: void pushState(); void popAndNotifyState(); - bool m_ownStream; QGstreamerPlayerSession *m_session; QMediaPlayer::State m_userRequestedState; QMediaPlayer::State m_currentState; diff --git a/src/plugins/qnx/mediaplayer/mmrenderermediaplayercontrol.cpp b/src/plugins/qnx/mediaplayer/mmrenderermediaplayercontrol.cpp index 30524106..3ba640cd 100644 --- a/src/plugins/qnx/mediaplayer/mmrenderermediaplayercontrol.cpp +++ b/src/plugins/qnx/mediaplayer/mmrenderermediaplayercontrol.cpp @@ -162,22 +162,6 @@ QByteArray MmRendererMediaPlayerControl::resourcePathForUrl(const QUrl &url) const QFileInfo fileInfo(relativeFilePath); return QFile::encodeName(QStringLiteral("file://") + fileInfo.absoluteFilePath()); - // QRC, copy to temporary file, as mmrenderer does not support resource files - } else if (url.scheme() == QStringLiteral("qrc")) { - const QString qrcPath = ':' + url.path(); - const QFileInfo resourceFileInfo(qrcPath); - m_tempMediaFileName = QDir::tempPath() + QStringLiteral("/qtmedia_") + - QUuid::createUuid().toString() + QStringLiteral(".") + - resourceFileInfo.suffix(); - if (!QFile::copy(qrcPath, m_tempMediaFileName)) { - const QString errorMsg = QString("Failed to copy resource file to temporary file " - "%1 for playback").arg(m_tempMediaFileName); - qDebug() << errorMsg; - emit error(0, errorMsg); - return QByteArray(); - } - return QFile::encodeName(m_tempMediaFileName); - // HTTP or similar URL } else { return url.toEncoded(); @@ -187,7 +171,7 @@ QByteArray MmRendererMediaPlayerControl::resourcePathForUrl(const QUrl &url) void MmRendererMediaPlayerControl::attach() { // Should only be called in detached state - Q_ASSERT(m_audioId == -1 && !m_inputAttached && m_tempMediaFileName.isEmpty()); + Q_ASSERT(m_audioId == -1 && !m_inputAttached); if (m_media.isNull() || !m_context) { setMediaStatus(QMediaPlayer::NoMedia); @@ -251,10 +235,6 @@ void MmRendererMediaPlayerControl::detach() } } - if (!m_tempMediaFileName.isEmpty()) { - QFile::remove(m_tempMediaFileName); - m_tempMediaFileName.clear(); - } m_loadingTimer.stop(); } diff --git a/src/plugins/qnx/mediaplayer/mmrenderermediaplayercontrol.h b/src/plugins/qnx/mediaplayer/mmrenderermediaplayercontrol.h index d4ddf363..79fc9be0 100644 --- a/src/plugins/qnx/mediaplayer/mmrenderermediaplayercontrol.h +++ b/src/plugins/qnx/mediaplayer/mmrenderermediaplayercontrol.h @@ -156,7 +156,6 @@ private: bool m_inputAttached; int m_stopEventsToIgnore; int m_bufferLevel; - QString m_tempMediaFileName; QTimer m_loadingTimer; }; diff --git a/tests/auto/unit/qmediaplayer/qmediaplayer.pro b/tests/auto/unit/qmediaplayer/qmediaplayer.pro index 52568c07..cbdbf71f 100644 --- a/tests/auto/unit/qmediaplayer/qmediaplayer.pro +++ b/tests/auto/unit/qmediaplayer/qmediaplayer.pro @@ -2,6 +2,7 @@ CONFIG += testcase no_private_qt_headers_warning TARGET = tst_qmediaplayer QT += network multimedia-private testlib SOURCES += tst_qmediaplayer.cpp +RESOURCES += testdata.qrc include (../qmultimedia_common/mock.pri) include (../qmultimedia_common/mockplayer.pri) diff --git a/tests/auto/unit/qmediaplayer/testdata.qrc b/tests/auto/unit/qmediaplayer/testdata.qrc new file mode 100644 index 00000000..1afc630d --- /dev/null +++ b/tests/auto/unit/qmediaplayer/testdata.qrc @@ -0,0 +1,5 @@ + + + testdata/nokia-tune.mp3 + + diff --git a/tests/auto/unit/qmediaplayer/testdata/nokia-tune.mp3 b/tests/auto/unit/qmediaplayer/testdata/nokia-tune.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..2435f65b8be6bd2402e51859a411e6ef5402d9f6 GIT binary patch literal 62715 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%0AR| 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 player->play(); QCOMPARE(ss.count(), 1); + QCOMPARE(ms.count(), 1); + QCOMPARE(qvariant_cast(ms.last().value(0)), QMediaPlayer::LoadingMedia); + ms.clear(); mockService->setState(QMediaPlayer::StoppedState, QMediaPlayer::InvalidMedia); QCOMPARE(player->state(), QMediaPlayer::PlayingState); - QCOMPARE(player->mediaStatus(), QMediaPlayer::InvalidMedia); + QCOMPARE(player->mediaStatus(), QMediaPlayer::LoadingMedia); QCOMPARE(ss.count(), 1); - QCOMPARE(ms.count(), 1); + QCOMPARE(ms.count(), 2); + QCOMPARE(qvariant_cast(ms.at(0).value(0)), QMediaPlayer::InvalidMedia); + QCOMPARE(qvariant_cast(ms.at(1).value(0)), QMediaPlayer::LoadingMedia); // NOTE: status should begin transitioning through to BufferedMedia. QCOMPARE(player->currentMedia(), content1); @@ -1210,5 +1217,84 @@ void tst_QMediaPlayer::testSupportedMimeTypes() // This is empty on some platforms, and not on others, so can't test something here at the moment. } +void tst_QMediaPlayer::testQrc_data() +{ + QTest::addColumn("mediaContent"); + QTest::addColumn("status"); + QTest::addColumn("error"); + QTest::addColumn("errorCount"); + QTest::addColumn("hasStreamFeature"); + QTest::addColumn("backendMediaContentScheme"); + QTest::addColumn("backendHasStream"); + + QTest::newRow("invalid") << QMediaContent(QUrl(QLatin1String("qrc:/invalid.mp3"))) + << QMediaPlayer::InvalidMedia + << QMediaPlayer::ResourceError + << 1 // error count + << false // No StreamPlayback support + << QString() // backend should not have got any media (empty URL scheme) + << false; // backend should not have got any stream + + QTest::newRow("valid+nostream") << QMediaContent(QUrl(QLatin1String("qrc:/testdata/nokia-tune.mp3"))) + << QMediaPlayer::LoadingMedia + << QMediaPlayer::NoError + << 0 // error count + << false // No StreamPlayback support + << QStringLiteral("file") // backend should have a got a temporary file + << false; // backend should not have got any stream + + QTest::newRow("valid+stream") << QMediaContent(QUrl(QLatin1String("qrc:/testdata/nokia-tune.mp3"))) + << QMediaPlayer::LoadingMedia + << QMediaPlayer::NoError + << 0 // error count + << true // StreamPlayback support + << QStringLiteral("qrc") + << true; // backend should have got a stream (QFile opened from the resource) +} + +void tst_QMediaPlayer::testQrc() +{ + QFETCH(QMediaContent, mediaContent); + QFETCH(QMediaPlayer::MediaStatus, status); + QFETCH(QMediaPlayer::Error, error); + QFETCH(int, errorCount); + QFETCH(bool, hasStreamFeature); + QFETCH(QString, backendMediaContentScheme); + QFETCH(bool, backendHasStream); + + if (hasStreamFeature) + mockProvider->setSupportedFeatures(QMediaServiceProviderHint::StreamPlayback); + + QMediaPlayer player; + + mockService->setState(QMediaPlayer::PlayingState, QMediaPlayer::NoMedia); + + QSignalSpy mediaSpy(&player, SIGNAL(currentMediaChanged(QMediaContent))); + QSignalSpy statusSpy(&player, SIGNAL(mediaStatusChanged(QMediaPlayer::MediaStatus))); + QSignalSpy errorSpy(&player, SIGNAL(error(QMediaPlayer::Error))); + + player.setMedia(mediaContent); + + QTRY_COMPARE(player.mediaStatus(), status); + QCOMPARE(statusSpy.count(), 1); + QCOMPARE(qvariant_cast(statusSpy.last().value(0)), status); + + QCOMPARE(player.media(), mediaContent); + QCOMPARE(player.currentMedia(), mediaContent); + QCOMPARE(mediaSpy.count(), 1); + QCOMPARE(qvariant_cast(mediaSpy.last().value(0)), mediaContent); + + QCOMPARE(player.error(), error); + QCOMPARE(errorSpy.count(), errorCount); + if (errorCount > 0) { + QCOMPARE(qvariant_cast(errorSpy.last().value(0)), error); + QVERIFY(!player.errorString().isEmpty()); + } + + // Check the media actually passed to the backend + QCOMPARE(mockService->mockControl->media().canonicalUrl().scheme(), backendMediaContentScheme); + QCOMPARE(bool(mockService->mockControl->mediaStream()), backendHasStream); +} + QTEST_GUILESS_MAIN(tst_QMediaPlayer) #include "tst_qmediaplayer.moc" diff --git a/tests/auto/unit/qmultimedia_common/mockmediaplayercontrol.h b/tests/auto/unit/qmultimedia_common/mockmediaplayercontrol.h index 5b313fb9..5127498b 100644 --- a/tests/auto/unit/qmultimedia_common/mockmediaplayercontrol.h +++ b/tests/auto/unit/qmultimedia_common/mockmediaplayercontrol.h @@ -91,11 +91,10 @@ public: { _stream = stream; _media = content; - if (_state != QMediaPlayer::StoppedState) { - _mediaStatus = _media.isNull() ? QMediaPlayer::NoMedia : QMediaPlayer::LoadingMedia; + _mediaStatus = _media.isNull() ? QMediaPlayer::NoMedia : QMediaPlayer::LoadingMedia; + if (_state != QMediaPlayer::StoppedState) emit stateChanged(_state = QMediaPlayer::StoppedState); - emit mediaStatusChanged(_mediaStatus); - } + emit mediaStatusChanged(_mediaStatus); emit mediaChanged(_media = content); } QIODevice *mediaStream() const { return _stream; } diff --git a/tests/auto/unit/qmultimedia_common/mockmediaserviceprovider.h b/tests/auto/unit/qmultimedia_common/mockmediaserviceprovider.h index 60ea672c..89820c55 100644 --- a/tests/auto/unit/qmultimedia_common/mockmediaserviceprovider.h +++ b/tests/auto/unit/qmultimedia_common/mockmediaserviceprovider.h @@ -61,6 +61,16 @@ public: } } + QMediaServiceProviderHint::Features supportedFeatures(const QMediaService *) const + { + return features; + } + + void setSupportedFeatures(QMediaServiceProviderHint::Features f) + { + features = f; + } + QByteArray defaultDevice(const QByteArray &serviceType) const { if (serviceType == Q_MEDIASERVICE_CAMERA) @@ -97,6 +107,7 @@ public: QMediaService *service; bool deleteServiceOnRelease; + QMediaServiceProviderHint::Features features; }; #endif // MOCKMEDIASERVICEPROVIDER_H