QML Book 第十章 多媒体

10.多媒体(Multimedia

本章的作者:e8johan

** 注意: **
最新的构建时间:2016/03/21
这章的源代码能够在assetts folder找到。

Qt 多媒体模块中的多媒体元素使得可以播放和记录诸如声音、视频或图片的媒体。解码和编码通过平台特定的后端来处理。例如,在 Linux 平台上使用流行的 gstreamer 框架,而在 Windows 上使用 DirectShow,在 OS X 上使用 QuickTime。

多媒体元素不是 Qt Quick core API 的一部分。相反,它们需要通过导入 QtMultimedia 5.6 提供的单独的 API 提供,如下所示:

import QtMultimedia 5.6

10.1 播放媒体

QML 应用程序中最基本的多媒体集成是用于播放媒体。这可以使用 MediaPlayer 元素完成,如果源是图像或视频,则可以与 VideoOutput 元素组合使用。MediaPlayer 元素具有指向要播放的媒体的源(source)属性。媒体源被绑定后,只需调用播放(play)功能即可开始播放。

如果要播放视频媒体,即图片或视频,我们还必须设置一个 VideoOutput 元素。运行播放的 MediaPlayer 将通过 source 属性绑定到视频输出。

在下面的示例中,MediaPlayer 被给予一个视频内容作为源的文件。VideoOutput 被创建并绑定到媒体播放器。一旦主要组件被完全初始化,在 Component.onCompleted 中,播放器的播放功能会被调用。

import QtQuick 2.5
import QtMultimedia 5.6

Item {
    width: 1024
    height: 600

    MediaPlayer {
        id: player
        source: "trailer_400p.ogg"
    }

    VideoOutput {
        anchors.fill: parent
        source: player
    }

    Component.onCompleted: {
        player.play();
    }
}

通过 MediaPlayer 元素的 volume 属性来控制播放媒体时改变音量的基本操作。还有其他有用的属性。例如,持续时间(duration)和位置(position)属性可用于构建进度条。如果可定位(seekable)属性为真(true),甚至可以在进度条被轻敲时更新位置(position)。下面的示例显示了如何添加到上面的基本播放示例。

    Rectangle {
        id: progressBar

        anchors.left: parent.left
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        anchors.margins: 100

        height: 30

        color: "lightGray"

        Rectangle {
            anchors.left: parent.left
            anchors.top: parent.top
            anchors.bottom: parent.bottom

            width: player.duration>0?parent.width*player.position/player.duration:0

            color: "darkGray"
        }

        MouseArea {
            anchors.fill: parent

            onClicked: {
                if (player.seekable) {
                    player.position = player.duration * mouse.x/width;
                }
            }
        }
    }

在默认情况下,position 属性只能每秒更新一次。这意味着进度条将以较大的步骤进行更新,这样看起来播放进度条是一跳一跳的,除非与进度条的宽度相对应的像素数相比,媒体的持续时间足够长。这时,可以通过访问 mediaObject 属性及其 notifyInterval 属性来更改更新间隔。它可以设置为每个位置更新之间的毫秒数,我们可以根据媒体的实际时间来调整更新的时长,以此增加用户界面的平滑度。

    Connections {
        target: player
        onMediaObjectChanged: {
            if (player.mediaObject) {
                player.mediaObject.notifyInterval = 50;
            }
        }
    }

当使用 MediaPlayer 构建媒体播放器时,监视播放器的状态(status)属性是很好的选择。它是对可能的状态的枚举,范围从 MediaPlayer.Buffered 到 MediaPlayer.InvalidMedia。以下项目中总结了可能的值:

  • MediaPlayer.UnknownStatus —— 状态未知。
  • MediaPlayer.NoMedia —— 播放器没有分配媒体源。播放停止。
  • MediaPlayer.Loading —— 播放器正在加载媒体。
  • MediaPlayer.Loaded —— 媒体已经加载。播放停止。
  • MediaPlayer.Stalled —— 媒体的装载停滞了。
  • MediaPlayer.Buffering —— 正在缓冲媒体。
  • MediaPlayer.Buffered —— 媒体被缓冲,这意味着用户可以开始播放媒体。
  • MediaPlayer.EndOfMedia —— 媒体已经结束。播放停止。
  • MediaPlayer.InvalidMedia —— 媒体无法播放。播放停止。

如上述枚举值所述,播放状态可随时间而变化。调用 play、pause 或 stop 更改状态,但有问题的媒体也可以有效果。例如,可以到达结束,否则可能无效,导致播放停止。当前播放状态可以通过 playbackState 属性进行跟踪。值可以是 MediaPlayer.PlayingState、MediaPlayer.PausedState 或 MediaPlayer.StoppedState。

使用 autoPlay 属性,MediaPlayer 可以在 source 属性更改后尝试进入播放状态。类似的属性是 autoLoad,导致播放器在 source 属性更改后立即尝试加载媒体。后一个属性默认是启用的。

也可以让 MediaPlayer 循环播放媒体项目。循环(loops)属性控制源播放的次数。将属性设置为 MediaPlayer.Infinite 会设置为无限循环。这对连续动画或循环的背景歌曲的播放是很有效的。

10.2 声音特效

当播放声音效果时,从请求播放到实际播放的响应时间变得很重要。在这种情况下,SoundEffect元素派上用场。通过设置源(source)属性,对播放(play)功能的简单调用立即开始播放。

这可以用于在点击屏幕时进行音频反馈,如下所示:

    SoundEffect {
        id: beep
        source: "beep.wav"
    }

    Rectangle {
        id: button

        anchors.centerIn: parent

        width: 200
        height: 100

        color: "red"

        MouseArea {
            anchors.fill: parent
            onClicked: beep.play()
        }
    }

该元素也可以用于伴随音频的转换。要从转换触发播放,将使用 ScriptAction 元素。

    SoundEffect {
        id: swosh
        source: "swosh.wav"
    }

    transitions: [
        Transition {
            ParallelAnimation {
                ScriptAction { script: swosh.play(); }
                PropertyAnimation { properties: "rotation"; duration: 200; }
            }
        }
    ]

除了 play 功能之外,还有许多类似的 MediaPlayer 提供的属性。示例是 volume 和 loops。后者可以设置为 SoundEffect.Infinite 进行无限播放。要停止播放,请调用 stop 功能。

** 注意: **

当使用 PulseAudio 后端时,stop 不会立即停止,但只能防止进一步的循环。这是由于底层 API 的限制。

10.3 视频流

VideoOutput 元素不限于与 MediaPlayer 元素组合使用。它也可以直接与视频源一起使用来显示直播视频流。使用 Camera 元素作为源,并且应用程序已完成。来自相机的视频流可以用于向用户提供直播流。此流在捕获照片时用作搜索视图。

import QtQuick 2.5
import QtMultimedia 5.6

Item {
    width: 1024
    height: 600

    VideoOutput {
        anchors.fill: parent
        source: camera
    }

    Camera {
        id: camera
    }
}

10.4 捕获图像

相机(Camera)元素的主要功能之一是可用于拍摄照片。我们将在简单的停止运动应用程序中使用它。在其中,我们将学习如何显示取景器,拍摄照片和跟踪拍摄的照片。

用户界面如下所示。它由三个主要部分组成。在后台,我们将在右侧找到一个取景器,一列按钮,底部显示拍摄的图像列表。想法是拍摄一系列照片,然后点击顺序播放按钮。这将播放图像,创建一个简单的幻灯片。

camera-ui

相机的取景器部分只是一个 Camera 元素,用作 VideoOutput 中的源。这将向用户显示相机的直播视频流。

    VideoOutput {
        anchors.fill: parent
        source: camera
    }

    Camera {
        id: camera
    }

照片列表是一个 ListView,横向显示来自 ListModel 的图像,名为 imagePaths。在背景中,使用半透明的黑色矩形(Rectangle)。

    ListModel {
        id: imagePaths
    }

    ListView {
        id: listView

        anchors.left: parent.left
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        anchors.bottomMargin: 10

        height: 100

        orientation: ListView.Horizontal
        spacing: 10

        model: imagePaths

        delegate: Image {
            height: 100
            source: path
            fillMode: Image.PreserveAspectFit
        }

        Rectangle {
            anchors.fill: parent
            anchors.topMargin: -10

            color: "black"
            opacity: 0.5
        }
    }

为了拍摄图像,我们需要知道 Camera 元素包含一组用于各种任务的子元素。要捕获静态图片,使用 Camera.imageCapture 元素。当我们调用捕获(capture)方法时,将拍摄照片。这将导致 Camera.imageCapture 首先发出 imageCaptured 信号,后跟 imageSaved 信号。

        Button {
            id: shotButton

            text: "Take Photo"
            onClicked: {
                camera.imageCapture.capture();
            }
        }

为了拦截子元素的信号,需要一个 Connections 元素。在这种情况下,我们不需要显示预览图像,而只是将生成的图像添加到屏幕底部的 ListView。 如下面的示例所示,保存的图像的路径作为带有信号的路径(path)参数提供。

    Connections {
        target: camera.imageCapture

        onImageSaved: {
            imagePaths.append({"path": path})
            listView.positionViewAtEnd();
        }
    }

要显示预览,请连接到 imageCaptured 信号,并使用预览(preview)信号参数作为 Image 元素的源(source )。 requestId 信号参数同时发送 imageCaptured 和 imageSaved。该值从捕获(capture)方法返回。使用它,捕获图像可以在整个循环中进行跟踪。这样,预览可以先使用,然后被正确保存的图像替换。但是,这并不是我们在这个例子中所做的。

应用程序的最后一部分是实际播放。这是使用 Timer 元素和一些 JavaScript 驱动的。 _imageIndex 变量用于跟踪当前显示的图像。当显示最后一张图像时,播放停止。在该示例中,root.state 用于在播放序列时隐藏用户界面的部分。

    property int _imageIndex: -1

    function startPlayback()
    {
        root.state = "playing";
        setImageIndex(0);
        playTimer.start();
    }

    function setImageIndex(i)
    {
        _imageIndex = i;

        if (_imageIndex >= 0 && _imageIndex < imagePaths.count)
            image.source = imagePaths.get(_imageIndex).path;
        else
            image.source = "";
    }

    Timer {
        id: playTimer

        interval: 200
        repeat: false

        onTriggered: {
            if (_imageIndex + 1 < imagePaths.count)
            {
                setImageIndex(_imageIndex + 1);
                playTimer.start();
            }
            else
            {
                setImageIndex(-1);
                root.state = "";
            }
        }
    }

10.5 实用技巧

10.5.1 实现播放列表

Qt 5 多媒体 API 不支持播放列表。幸运的是,建立一个却很容易。这个想法是能够使用一个项目模型对一个 MediaPlayer 元素进行设置,如下所示。播放列表元素可用于设置 MediaPlayer 的源(source),而播放状态通过播放器进行控制。

    MediaPlayer {
        id: player
        playlist: Playlist {
            PlaylistItem { source: "trailer_400p.ogg" }
            PlaylistItem { source: "trailer_400p.ogg" }
            PlaylistItem { source: "trailer_400p.ogg" }
        }
    }

播放列表(Playlist)元素的上半部分,如下所示,负责在 setIndex 函数中设置给定索引的源(source)元素。它还实现了 next 和 previous 的功能来导航列表。

Item {
    id: root

    property int index: 0
    property MediaPlayer mediaPlayer
    property ListModel items: ListModel {}

    function setIndex(i) {
        console.log("setting index to: " + i);

        index = i;

        if (index < 0 || index >= items.count) {
            index = -1;
            mediaPlayer.source = "";
        } else {
            mediaPlayer.source = items.get(index).source;
        }
    }

    function next() {
        setIndex(index + 1);
    }

    function previous() {
        setIndex(index + 1);
    }

使播放列表继续到每个元素末尾的下一个元素的技巧是监视 MediaPlayer 的状态(status)属性。达到 MediaPlayer.EndOfMedia 状态后,索引增加并恢复播放,或者如果达到列表的结尾,播放停止。

    Connections {
        target: root.mediaPlayer

        onStopped: {
            if (root.mediaPlayer.status == MediaPlayer.EndOfMedia) {
                root.next();
                if (root.index == -1) {
                    root.mediaPlayer.stop();
                } else {
                    root.mediaPlayer.play();
                }
            }
        }
    }

10.6 总结

Qt 提供的多媒体 API 提供了播放和捕获视频和音频的机制。通过 VideoOutput 元素,视频源可以显示在用户界面中。通过 MediaPlayer 元素,可以操作大多数的播放,SoundEffect 可以用于低延迟的声音播放。为了捕获图像或显示实时视频流,可以使用 Camera 元素。

本章完,欢迎提出建议和指正翻译问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容