Bonjour,
Aujourd'hui je vais vous exposer une solution Qt Quick permettant de jouer une video en plein écran avec contrôle du lecteur (Play/Pause), réglage du son avec les touches matérielles ou depuis l'interface tactile via un slider. Par ailleurs je forcerai le mode "paysage" pour lire la vidéo ; pour le changement d'orientation de l'écran je vous invite à lire mon précédent article.
L'application se décompose en plusieurs écrans :
- Une page d'accueil avec trois boutons :
- un bouton "local video" permettant de lire une video locale embarquée dans le binaire
- un bouton "remote video" permettant de lire une video en streaming
- un bouton "quitter"
2. Visonnage de la video locale sans barre de contrôle :
3. Visionnage de la video locale avec barres de contrôle en overlay :
4. Ecran de chargement de la vidéo remote (streaming) :
5. Lecture la video en streaming :
Commençons par le point d'entrée, à savoir la fonction main du fichier main.cpp :
int main
(int argc, char *argv[])
{
QApplication app(argc, argv);
QmlApplicationViewer viewer;
viewer.setOrientation(QmlApplicationViewer::ScreenOrientationLockPortrait);
// Set the screen size to QML context
QDeclarativeContext* context = viewer.rootContext();
context->setContextProperty("appviewer", &viewer);
qmlRegisterType<MediakeyCaptureItem>("Mediakey", 1, 0, "MediakeyCapture");
qmlRegisterType<QmlApplicationViewer>("AppViewer", 1, 0, "QmlApplicationViewer");
viewer.setMainQmlFile(QLatin1String("qml/VideoTest/main.qml"));
viewer.showExpanded();
return app.exec();
}
Deux objets C++ importants sont déclarés dans le contexte déclaratif :
- un objet de type MediakeyCapture dont le rôle est d'intercepter les événements de touche hardware "Sound Up" et "Sound Down" situés latéralement sur les téléphones Nokia. Je vous invite à lire ce wiki très bien fait qui explique comment capturer les événements associés à ces touches (il s'agit d'un hack symbian car les signaux Keys.OnVolumeDownPressed et Keys.OnVolumeUpPressed ne sont pas remontés au niveau QML)
- un objet de type QmlApplicationViewer permettant de forcer le mode d'orientation de manière programmatique. Pour la description complète de cette classe, je vous invite à lire l'article correspondant ici
Côté QML, j'ai fait à l'ancienne sans utiliser les pages stacks des composants Qt Quick pour Symbian : j'ai écrit deux fichiers QML :
- Main.qml : il décrit la page principale avec ces trois boutons.
- VideoPlayer.qml : il décrit le player video en mode fullscreen paysage
Détail de Main.qml
C'est un rectangle dimensionné selon le terminal de test (640x360) dans mon cas :
Rectangle {
id: main
width: 360
height: 640
Avec la définition d'un signal et de son slot associé utilisé pour lancer le lecteur video :
signal displayVideoPlayer(string video_url)
onDisplayVideoPlayer: {
console.log("onDisplayVideoPlayer");
mainLoader.source = "VideoPlayer.qml"
mainLoader.item.source = video_url
mainLoader.item.play()
main.state = "videoState"
appviewer.setOrientation(QmlApplicationViewer.ScreenOrientationLockLandscape)
}
Pour lancer le player video :
- j'utilise un élément Loader (identifiant "mainLoader") qui permet de charger à la demande une ressource QML (fichier QML dénommé VideoPlayer.qml)
- L'objet VideoPlayer possède une propriété importante : l'url pointant sur la video (locale ou réseau)
- l'élément main dispose de deux états : "homeState" et "videoState" que je décrirais ci-dessous. Lorsque je lance la vidéo, je passe à l'état "videoState"
- je force l'écran à basculer en mode paysage
Ensuite il y a la définition des deux états :
state:"homeState"
states: [
State {
name: "videoState"
PropertyChanges {
target: mainView
visible: false
}
PropertyChanges {
target: mainLoader
visible: true
}
},
State {
name: "homeState"
PropertyChanges {
target: mainView
visible: true
}
PropertyChanges {
target: mainLoader
visible: false
}
}
]
Deux états pour deux écrans :
- un état "homeState" (état au démarrage) rendant visible la page d'accueil avec les trois boutons et masquant le loader dont la source correspond au lecteur video
- un état "videoState" masquant la page d'accueil au profit du lecteur video
Ensuite vient la construction de la page d'accueil avec les trois boutons :
Rectangle{
id: mainView
anchors.fill: parent
// un gradient pour faire joli
gradient: Gradient {
GradientStop {
position: 0.0
color: "#c5cc94"
}
GradientStop {
position: 0.8
color: "#438048"
}
GradientStop {
position: 1
color: "#000000"
}
}
// un bouton pour lancer la video locale
Button {
anchors.bottom: launchRemoteVideoButton.top
anchors.bottomMargin: 40
anchors.horizontalCenter: parent.horizontalCenter
text:"Local video"
width: 170
onClicked: {
main.displayVideoPlayer("video.mp4");
}
}
// un bouton pour lancer la video réseau
Button {
id: launchRemoteVideoButton
anchors.centerIn: parent
text:"Remote video"
width: 170
onClicked: {
main.displayVideoPlayer("http://iop1.nokia-boston.com/HTML5/Video/Phase1/the-ninja-cat.mp4");
}
}
// un bouton pour quitter
Button {
id: quitButton
anchors.top: launchRemoteVideoButton.bottom
anchors.topMargin: 40
anchors.horizontalCenter: parent.horizontalCenter
text:"Quit"
width: 170
onClicked: {
Qt.quit()
}
}
}
J'utilise donc le composant Qt Quick pour Symbian "Bouton" avec appel du signal "displayVideoPlayer" sur le slot "onClicked" de manière à déclencher l'affichage du lecteur video.
Enfin je créé le loader permettant de charger le lecteur video de manière dynamique :
Loader{
z: 1
id: mainLoader
anchors.fill: parent
}
Connections{
target: mainLoader.item
ignoreUnknownSignals: true
onVideoChangeBackTo:{
console.log("Return to home page")
appviewer.setOrientation(QmlApplicationViewer.ScreenOrientationLockPortrait)
mainLoader.source=""
main.state="homeState"
}
}
Cet élément de connexion au signal "VideoChangeBackTo" me permet de traiter le retour à la page d'accueil lorsque la vidéo s'est terminée : je repositionne l'écran en mode paysage et je bascule dans l'état "homeState".
Détail de VideoPlayer.qml
Le player video est une zone rectangulaire avec un fond noir et une méthode "play" pour démarrer le visionnage de la vidéo et un signal "videoChangeBackto" pour indiquer la fin de ce même visionnage :
import QtQuick 1.1
import QtMultimediaKit 1.1
import com.nokia.symbian 1.1 // Symbian components
import Mediakey 1.0
Rectangle {
id: videoPlayer
property alias source: video.source
property real opacityValue: 0.6
signal videoChangeBackTo()
function play(){
video.play()
}
width: 640
height: 360
color: "black"
Comme nous allons le voir d'ici peu, "video" est l'identifiant de l'élément QML Video. opacityValue correspond au degré de transparence des barres de contrôles en overlay.
Ensuite je définis deux états pour controller l'affichage des barres de contrôle du lecteur vidéo :
state:
"hideControlBar"
states: [
State {
name: "showControlBar"
PropertyChanges {
target: topControlBar
visible:true
}
PropertyChanges {
target: bottomControlBar
visible:true
}
PropertyChanges {
target: videoPlayerMouseArea
enabled:false
}
},
State {
name: "hideControlBar"
PropertyChanges {
target: topControlBar
visible:false
}
PropertyChanges {
target: bottomControlBar
visible:false
}
PropertyChanges {
target: videoPlayerMouseArea
enabled:true
}
}
]
topControlBar est l'identifiant de la barre de contrôle supérieure contenant le bouton de fermeture et le slider pour ajuster le voume sonore. bottomControlBar est l'identifiant de la barre de contrôle inférieure contenant le bouton play/pause ainsi que la barre de progression temporelle. Enfin "videoPlayerMouseArea" correspond à toute la zone interactive du lecteur video.
Globalement l'état "showControlBar" définit le lecteur video avec les barres visibles en overlay (et la zone "videoPlayerMouseArea" désactivée et l'état "hideControlBar" définit le lecteur video sans les barres de contrôle mais avec toute la surface tactile activée. Dit simplement, ce mécanisme à deux états permet d'afficher les contrôles lorsque l'utilisateur clique sur la video. Par ailleurs la durée d'affichage de ces barres de contrôles est conditionnée à l'expiration d'un timer de 10 secondes comme le montre la suite du code :
Timer{
id:controlBarTimer
interval:10000
running:true
onTriggered:{videoPlayer.state="hideControlBar"}
}
Vient ensuite un élément indispensable : le fameux élément QML Video :
Video{
id: video
anchors.centerIn: parent
width: parent.width
height: parent.height
Component.onCompleted: {
console.log("Video Component.onCompleted")
volume = 0.2
videoPlayer.state="hideControlBar"
loadingScreen.state = "showLoadingScreen"
}
onStopped:
{
console.log("Video onStopped signal received")
mediaCapture.destroy()
videoPlayer.videoChangeBackTo()
loadingScreen.state = "hideLoadingScreen"
}
onStatusChanged:
{
console.log("Video onStatusChanged signal received")
console.log("Status : "+status)
if(status == Video.NoMedia)
{
console.log("Status : NoMedia")
}
else if(status == Video.Loading)
{
console.log("Status : Loading")
}
else if(status == Video.Loaded)
{
console.log("Status : Loaded")
loadingScreen.state = "hideLoadingScreen"
}
else if(status == Video.Buffering)
{
console.log("Status : Buffering")
}
else if(status == Video.Stalled)
{
console.log("Status : Stalled")
}
else if(status == Video.Buffered)
{
console.log("Status : Buffered")
}
else if(status == Video.EndOfMedia)
{
console.log("Status : EndOfMedia")
}
else if(status == Video.InvalidMedia)
{
console.log("Status : InvalidMedia")
}
else if(status == Video.UnknownStatus)
{
console.log("Status : UnknownStatus")
}
}
}
Cette surface video prend tout l'écran et voit son initialisation réalisée après son chargement (qui correspond à l'émission du signal "onCompleted". Dans le slot associé, je positionne le volume à 0.2 (soit 20% du volume max) car par défaut le son est au maximum sur Symbian, ce qui est intrusif lorsqu'on lit une video dans le métro. Lorsque le player se lance, je masque les barres de contrôles et j'affiche l'écran de chargement.
Ensuite je définis les gestionnaires des signaux "Stopped" et "StatusChanged" :
- Signal Stopped : je détruis l'objet "mediaCapture" (afin qu'il soit réutilisable lors du prochain visionnage) ; j'émets le signal videoChangeBackTo afin que ce dernier soit récupéré par l'élément QML Main pour afficher la page d'accueil
- Signal StatusChanged : je m'intéresse uniquement à l'état "Loaded" du lecteur afin de masquer l'acran de chargement quand la video est prête à s'afficher. Noter que dans le carde d'une application plus robuste, il est nécessaire de monitorer les différents status, notamment pour la gestion d'erreurs
Je mets en place mon objet "MediaCapture" pour intercepter les événements matériels "Sound Up" et "Sound Down" :
MediakeyCapture{
id: mediaCapture
onVolumeDownPressed:{
console.log('VOLUME DOWN PRESSED ')
if(video.volume>0.1) video.volume -= 0.1
}
onVolumeUpPressed:{
console.log('VOLUME UP PRESSED ')
if(video.volume<1) video.volume += 0.1
}
}
Rien de bien compliqué ici : j'ajuste le volume en conséquence. Vous verrez par la suite que grâce au binding de la propriété "volume" avec le slider visuel de contrôle de volume, ce dernier ce mettra automatiquement à jour lorsqu'on ajustera le volume depuis ces touches claviers.
Après je définis mon écran de chargement avec une indicateur animé (composant BusyIndicator) :
Rectangle{
id: loadingScreen
anchors.centerIn: parent
color: "black"
visible: true
state: "showLoadingScreen"
states: [
State {
name: "showLoadingScreen"
PropertyChanges {
target: loadingScreen
visible:true
z:1
}
},
State {
name: "hideLoadingScreen"
PropertyChanges {
target: loadingScreen
visible:false
z:0
}
}
]
Text{
id: bgText
anchors.centerIn: parent
color: "white"
text: "Chargement de la vidéo ..."
}
BusyIndicator {
id: indicator
anchors.verticalCenter: parent.verticalCenter
anchors.right: bgText.left
anchors.rightMargin: 20
running: true
}
}
Noter la définition de deux états "showLoadingScreen" et "hideLoadingScreen" permettant de jouer sur la visibilité de cette page de chargement.
Ensuite je définis la barre de contrôle supérieure avec le bouton de fermetture et le slider d'ajustement de volume :
Rectangle {
id: topControlBar
height: 66
width: parent.width
color: "black"
opacity: parent.opacityValue
anchors.top:parent.top
Button {
id: closeButton
iconSource: "images/close.png"
width: 64
height: 64
anchors{left:parent.left;leftMargin:10;}
anchors.verticalCenter: parent.verticalCenter
MouseArea{
anchors.fill: parent
onClicked: {
videoPlayer.state="hideControlBar"
video.stop()
}
}
}
Image {
id: soundOff
source: "images/sound_off.png"
anchors.left:closeButton.right
anchors.leftMargin:20
anchors.verticalCenter: parent.verticalCenter
width: 48
height: 48
}
Slider {
id:soundBar
anchors{left:soundOff.right;leftMargin:10;}
anchors.verticalCenter: parent.verticalCenter
height: 10
width: 640-x-soundOn.width-20
maximumValue: 1.0
minimumValue: 0
value: video.volume
stepSize: 0.1
valueIndicatorVisible: true
valueIndicatorText: "Volume"
onValueChanged: {
console.log(value)
video.volume = value
}
MouseArea{
anchors.fill: parent
}
}
Image {
id: soundOn
source: "images/sound_on.png"
anchors{left:soundBar.right;leftMargin:10;}
anchors.verticalCenter: parent.verticalCenter
width: 48
height: 48
}
}
Les points principaux à noter concernant le code ci-dessus sont :
- Le gestionnaire de clic sur le bouton "close" : on masque les barres de contrôle et on arrête le lecteur video
- Le binding de la propriété "value" du slider avec le volume
On continue avec cette fois la barre de contrôle inférieure affichant le bouton Play/Pause ainsi que la barre de progression temporelle :
Rectangle {
id: bottomControlBar
height: 66
width: parent.width
color: "black"
opacity: parent.opacityValue
anchors.bottom: parent.bottom
Button {
id: playPause
width: 64
height:64
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
state: "clicked"
states: [
State{
name: "clicked"
PropertyChanges {
target: playPause
iconSource: "images/pause.png"
}
},
State{
name: "unclicked"
PropertyChanges {
target: playPause
iconSource: "images/play.png"
}
}
]
onClicked: {
if(video.paused==false && video.playing==true)
{
controlBarTimer.stop()
videoPlayer.state="showControlBar"
video.pause()
playPause.state="unclicked"
}
else {
controlBarTimer.restart()
video.play()
playPause.state="clicked"
}
}
}
Slider {
id:progressBar
anchors{left:playPause.right;leftMargin:20;}
anchors.verticalCenter: parent.verticalCenter
height: 10
width: 640-20-x
maximumValue: video.duration
minimumValue: 0
value: video.position
stepSize: 1000
valueIndicatorVisible: true
valueIndicatorText:convertToMinuteSecond(video.position)+" / "+convertToMinuteSecond(video.duration)
onPressedChanged:
{
if(!pressed)
{
video.position = value
}
}
onValueChanged: {
console.log(value)
}
function convertToMinuteSecond(ms){
var ti=ms
var myDate = new Date(ti);
return (myDate.getMinutes()<10?("0"+myDate.getMinutes()):myDate.getMinutes())+":"+(myDate.getSeconds()<10?("0"+myDate.getSeconds()):myDate.getSeconds());
}
MouseArea{
anchors.fill: parent
}
}
}
Noter le binding de la bulle (propriété
valueIndicatorText ) avec la méthode
convertToMinuteSecond permettant de convertir la valeur temporelle écoulée en minute/seconde. Lorsqu'on relâche l'indicateur du slider, la position de la video est automatiquement modifiée.
Enfin pour conclure, il y a l'élément MouseArea permettant de faire apparaître les barres de contrôle en overlay :
MouseArea{
id:videoPlayerMouseArea
anchors.fill: parent
onClicked: {
videoPlayer.state="showControlBar"
controlBarTimer.start()
}
}
L'apparition des barres de contrôles sont déclenchées par timer pour une durée maximum de 10 secondes.
C'est fini : le code complet est disponible
ici (pour des raisons de taille de stockage, j'ai dû supprimer la vidéo locale)