1/* 2 * Copyright (C) 2016-2019 3 * Jean-Luc Barriere <jlbarriere68@gmail.com> 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; version 3. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18import QtQuick 2.9 19import NosonApp 1.0 20 21/* 22 * This file should *only* manage the media playing and the relevant settings 23 * It should therefore only access ZonePlayer, trackQueue and Settings 24 * Anything else within the app should use Connections to listen for changes 25 */ 26 27 28Item { 29 id: player 30 objectName: "controller" 31 property alias trackQueue: trackQueueLoader.item 32 property alias renderingModel: renderingModelLoader.item 33 property bool connected: false 34 property string zoneId: "" 35 property string zoneName: "" 36 property string zoneShortName: "" 37 property string controllerURI: "" 38 property string currentMetaAlbum: "" 39 property string currentMetaArt: "" 40 property string currentMetaArtist: "" 41 property string currentMetaSource: "" 42 property string currentMetaTitle: "" 43 property string currentMetaURITitle: "" 44 property int currentProtocol: -1 45 property int currentIndex: -1 46 property int currentCount: 0 47 property string playbackState: "" 48 readonly property bool isPlaying: (playbackState === "PLAYING") 49 property int trackPosition: 0 50 property int trackDuration: 0 51 property int volumeMaster: 0 52 property int bass: 0 53 property int treble: 0 54 property bool repeat: false 55 property bool shuffle: false 56 property bool mute: false 57 property bool outputFixed: false 58 property int renderingControlCount: 0 59 property bool sleepTimerEnabled: false 60 property bool nightmodeEnabled: false 61 property bool loudnessEnabled: false 62 property var covers: [] 63 64 property string queueInfo: queueOverviewString() 65 66 signal stopped() 67 signal sourceChanged() 68 signal currentPositionChanged(int position, int duration) 69 signal renderingControlChanged() // see function refreshRendering 70 71 onCurrentCountChanged: queueInfo = queueOverviewString() 72 onCurrentIndexChanged: queueInfo = queueOverviewString() 73 74 function connectZonePlayer(zonePlayer) { 75 connected = false; // force signal 76 var zp = zone.handle; 77 zp.disableMPRIS2(); 78 zone.handle = zonePlayer; 79 zone.handle.enableMPRIS2(); 80 zone.pid = zonePlayer.pid; 81 82 // execute all handlers to signal the changes 83 player.handleZPConnectedChanged(); 84 player.handleZPSourceChanged(); 85 player.handleZPRenderingCountChanged(); 86 player.handleZPRenderingGroupChanged(); 87 player.handleZPRenderingChanged(); 88 player.handleZPPlayModeChanged(); 89 player.handleZPPlaybackStateChanged(); 90 91 // release the old zone player 92 AllZonesModel.releasePlayer(zp); 93 } 94 95 function coordinatorName() { 96 return zone.handle.coordinatorName(); 97 } 98 99 function queueOverviewString() { 100 var str = ""; 101 if (!connected) 102 str = "- / -"; 103 else if (currentIndex < 0) 104 str = "- / " + currentCount; 105 else 106 str = (currentIndex + 1) + " / " + currentCount; 107 return str; 108 } 109 110 function ping(onFinished) { 111 if (connected) { 112 var future = zone.handle.tryPing(); 113 future.onFinished.connect(onFinished); 114 return future.start(); 115 } else { 116 onFinished(false); 117 } 118 } 119 120 function stop(onFinished) { 121 var future = zone.handle.tryStop(); 122 future.onFinished.connect(onFinished); 123 return future.start(); 124 } 125 126 function play(onFinished) { 127 var future = zone.handle.tryPlay(); 128 future.onFinished.connect(onFinished); 129 return future.start(); 130 } 131 132 function pause(onFinished) { 133 var future = zone.handle.tryPause(); 134 future.onFinished.connect(onFinished); 135 return future.start(); 136 } 137 138 function toggle(onFinished) { 139 if (isPlaying) 140 return pause(onFinished); 141 else 142 return play(onFinished); 143 } 144 145 function playSource(modelItem, onFinished) { 146 var future = zone.handle.tryPlaySource(modelItem.payload); 147 // hack: run animation now without waiting event 148 future.finished.connect(function(result) { 149 if (!result) 150 handleZPPlaybackStateChanged(); 151 onFinished(result); 152 }); 153 if (future.start()) { 154 player.playbackState = "TRANSITIONING"; 155 return true; 156 } 157 return false; 158 } 159 160 function previousSong(onFinished) { 161 var future = zone.handle.tryPrevious(); 162 future.onFinished.connect(onFinished); 163 return future.start(); 164 } 165 166 function nextSong(onFinished) { 167 var future = zone.handle.tryNext(); 168 future.onFinished.connect(onFinished); 169 return future.start(); 170 } 171 172 function toggleRepeat(onFinished) { 173 var future = zone.handle.tryToggleRepeat(); 174 future.onFinished.connect(onFinished); 175 return future.start(false); 176 } 177 178 function toggleShuffle(onFinished) { 179 var future = zone.handle.tryToggleShuffle(); 180 future.onFinished.connect(onFinished); 181 return future.start(false); 182 } 183 184 function seek(position, onFinished) { 185 if (player.canSeekInStream()) { 186 var future = zone.handle.trySeekTime(Math.floor(position / 1000)); 187 future.onFinished.connect(onFinished); 188 return future.start(); 189 } else { 190 onFinished(false); 191 } 192 } 193 194 function setVolumeGroupForFake(volume) { 195 return zone.handle.setVolumeGroup(volume, true); 196 } 197 198 function setVolumeForFake(uuid, volume) { 199 return zone.handle.setVolume(uuid, volume, true); 200 } 201 202 function setVolumeGroup(volume, onFinished) { 203 var future = zone.handle.trySetVolumeGroup(volume); 204 future.onFinished.connect(onFinished); 205 return future.start(false); 206 } 207 208 function setVolume(uuid, volume, onFinished) { 209 var future = zone.handle.trySetVolume(uuid, volume); 210 future.onFinished.connect(onFinished); 211 return future.start(false); 212 } 213 214 function setBass(value, onFinished) { 215 var future = zone.handle.trySetBass(value) 216 future.onFinished.connect(onFinished); 217 return future.start(false); 218 } 219 220 function setTreble(value, onFinished) { 221 var future = zone.handle.trySetTreble(value) 222 future.onFinished.connect(onFinished); 223 return future.start(false); 224 } 225 226 function toggleMuteGroup(onFinished) { 227 var future = zone.handle.tryToggleMute(); 228 future.onFinished.connect(onFinished); 229 return future.start(false); 230 } 231 232 function toggleMute(uuid, onFinished) { 233 var future = zone.handle.tryToggleMute(uuid); 234 future.onFinished.connect(onFinished); 235 return future.start(false); 236 } 237 238 function toggleNightmode(onFinished) { 239 var future = zone.handle.tryToggleNightmode(); 240 future.onFinished.connect(onFinished); 241 return future.start(false); 242 } 243 244 function toggleLoudness(onFinished) { 245 var future = zone.handle.tryToggleLoudness(); 246 future.onFinished.connect(onFinished); 247 return future.start(false); 248 } 249 250 function playQueue(start, onFinished) { 251 var future = zone.handle.tryPlayQueue(start); 252 future.onFinished.connect(onFinished); 253 return future.start(); 254 } 255 256 function seekTrack(nr, onFinished) { 257 var future = zone.handle.trySeekTrack(nr); 258 future.onFinished.connect(onFinished); 259 return future.start(); 260 } 261 262 function addItemToQueue(modelItem, nr, onFinished) { 263 var future = zone.handle.tryAddItemToQueue(modelItem.payload, nr); 264 future.onFinished.connect(onFinished); 265 return future.start(); 266 } 267 268 function makeFilePictureLocalURL(filePath) { 269 return zone.handle.makeFilePictureLocalURL(filePath); 270 } 271 272 function makeFileStreamItem(filePath, codec, title, album, author, duration, hasArt) { 273 return zone.handle.makeFileStreamItem(filePath, codec, title, album, author, duration, hasArt); 274 } 275 276 function addMultipleItemsToQueue(modelItemList, onFinished) { 277 var payloads = []; 278 for (var i = 0; i < modelItemList.length; ++i) 279 payloads.push(modelItemList[i].payload); 280 var future = zone.handle.tryAddMultipleItemsToQueue(payloads); 281 future.onFinished.connect(onFinished); 282 return future.start(); 283 } 284 285 function removeAllTracksFromQueue(onFinished) { 286 var future = zone.handle.tryRemoveAllTracksFromQueue(); 287 future.onFinished.connect(onFinished); 288 return future.start(); 289 } 290 291 function removeTrackFromQueue(modelItem, onFinished) { 292 var future = zone.handle.tryRemoveTrackFromQueue(modelItem.id, trackQueue.model.containerUpdateID()); 293 future.onFinished.connect(onFinished); 294 return future.start(); 295 } 296 297 function reorderTrackInQueue(nrFrom, nrTo, onFinished) { 298 var future = zone.handle.tryReorderTrackInQueue(nrFrom, nrTo, trackQueue.model.containerUpdateID()); 299 future.onFinished.connect(onFinished); 300 return future.start(); 301 } 302 303 function saveQueue(title, onFinished) { 304 var future = zone.handle.trySaveQueue(title); 305 future.onFinished.connect(onFinished); 306 return future.start(); 307 } 308 309 function createSavedQueue(title, onFinished) { 310 var future = zone.handle.tryCreateSavedQueue(title); 311 future.onFinished.connect(onFinished); 312 return future.start(); 313 } 314 315 function addItemToSavedQueue(playlistId, modelItem, containerUpdateID, onFinished) { 316 var future = zone.handle.tryAddItemToSavedQueue(playlistId, modelItem.payload, containerUpdateID); 317 future.onFinished.connect(onFinished); 318 return future.start(); 319 } 320 321 function addMultipleItemsToSavedQueue(playlistId, payloads, containerUpdateID, onFinished) { 322 var future = zone.handle.tryAddMultipleItemsToSavedQueue(playlistId, payloads, containerUpdateID); 323 future.onFinished.connect(onFinished); 324 return future.start(); 325 } 326 327 function removeTracksFromSavedQueue(playlistId, selectedIndices, containerUpdateID, onFinished) { 328 var future = zone.handle.tryRemoveTracksFromSavedQueue(playlistId, selectedIndices, containerUpdateID); 329 future.onFinished.connect(onFinished); 330 return future.start(); 331 } 332 333 function reorderTrackInSavedQueue(playlistId, index, newIndex, containerUpdateID, onFinished) { 334 var future = zone.handle.tryReorderTrackInSavedQueue(playlistId, index, newIndex, containerUpdateID); 335 future.onFinished.connect(onFinished); 336 return future.start(); 337 } 338 339 function configureSleepTimer(sec, onFinished) { 340 var future = zone.handle.tryConfigureSleepTimer(sec); 341 future.onFinished.connect(onFinished); 342 return future.start(false); 343 } 344 345 function remainingSleepTimerDuration(onFinished) { 346 var future = zone.handle.tryRemainingSleepTimerDuration(); 347 future.onFinished.connect(onFinished); 348 return future.start(false); 349 } 350 351 function playStream(url, title, onFinished) { 352 var future = zone.handle.tryPlayStream(url, (title === "" ? qsTr("Untitled") : title)); 353 // hack: run animation now without waiting event 354 future.finished.connect(function(result) { 355 if (!result) 356 handleZPPlaybackStateChanged(); 357 onFinished(result); 358 }); 359 if (future.start()) { 360 player.playbackState = "TRANSITIONING"; 361 return true; 362 } 363 return false; 364 } 365 366 function playLineIN(onFinished) { 367 var future = zone.handle.tryPlayLineIN(); 368 future.finished.connect(onFinished); 369 return future.start(); 370 } 371 372 function playDigitalIN(onFinished) { 373 var future = zone.handle.tryPlayDigitalIN(); 374 future.finished.connect(onFinished); 375 return future.start(); 376 } 377 378 function playPulse(onFinished) { 379 var future = zone.handle.tryPlayPulse(); 380 future.finished.connect(onFinished); 381 return future.start(); 382 } 383 384 function isPulseStream() { 385 return zone.handle.isPulseStream(currentMetaSource); 386 } 387 388 function isMyStream(metaSource) { 389 return zone.handle.isMyStream(metaSource); 390 } 391 392 function playFavorite(modelItem, onFinished) { 393 var future = zone.handle.tryPlayFavorite(modelItem.payload); 394 // hack: run animation now without waiting event 395 future.finished.connect(function(result) { 396 if (!result) 397 handleZPPlaybackStateChanged(); 398 onFinished(result); 399 }); 400 if (future.start()) { 401 player.playbackState = "TRANSITIONING"; 402 return true; 403 } 404 return false; 405 } 406 407 function syncTrackPosition() { 408 var future = zone.handle.tryCurrentTrackPosition(); 409 future.finished.connect(function(result) { 410 var npos = (result > 0 ? 1000 * result : 0); 411 player.trackPosition = npos > player.trackDuration ? 0 : npos; 412 //customdebug("sync position to " + player.trackPosition); 413 }); 414 future.start(false); 415 } 416 417 function isPlayingQueued() { 418 return player.trackDuration > 0; 419 } 420 421 function canSeekInStream() { 422 switch (currentProtocol) { 423 case 1: // x-rincon-stream 424 case 2: // x-rincon-mp3radio 425 case 5: // x-sonos-htastream 426 case 14: // http-get 427 return false; 428 default: 429 // the noson streamer uses the protocol http(17) 430 return isPlayingQueued(); 431 } 432 } 433 434 // reload the rendering model 435 // it should be triggered on signal renderingControlChanged 436 function refreshRendering() { 437 renderingModelLoader.item.load(zone.handle); 438 } 439 440 //////////////////////////////////////////////////////////////////////////// 441 // handlers connected to the ZP events 442 // they apply changes on the player properties and forward signals as needed 443 // 444 function handleZPConnectedChanged() { 445 player.connected = zone.handle.connected; 446 player.zoneId = zone.handle.zoneId; 447 player.zoneName = zone.handle.zoneName; 448 player.zoneShortName = zone.handle.zoneShortName; 449 player.controllerURI = zone.handle.controllerURI; 450 player.remainingSleepTimerDuration(function(result) { 451 player.sleepTimerEnabled = result > 0 ? true : false 452 }); 453 trackQueue.initQueue(zone.handle); 454 } 455 456 function handleZPSourceChanged() { 457 // protect against undefined properties 458 player.currentMetaAlbum = zone.handle.currentMetaAlbum || ""; 459 player.currentMetaArt = zone.handle.currentMetaArt || ""; 460 player.currentMetaArtist = zone.handle.currentMetaArtist || ""; 461 player.currentMetaSource = zone.handle.currentMetaSource || ""; 462 player.currentMetaTitle = zone.handle.currentMetaTitle || ""; 463 player.currentMetaURITitle = zone.handle.currentMetaURITitle || ""; 464 player.currentIndex = zone.handle.currentIndex; 465 player.currentProtocol = zone.handle.currentProtocol; 466 player.trackDuration = 1000 * zone.handle.currentTrackDuration; 467 // reset position 468 if (zone.handle.currentTrackDuration > 0) { 469 player.syncTrackPosition(); 470 } else { 471 player.trackPosition = 0; 472 } 473 // reset cover 474 if (player.currentProtocol == 1) { 475 player.covers = [{art: "qrc:/images/linein.png"}]; 476 } else if (player.currentProtocol == 5) { 477 player.covers = [{art: "qrc:/images/tv.png"}]; 478 } else { 479 player.covers = makeCoverSource(player.currentMetaArt, player.currentMetaArtist, player.currentMetaAlbum); 480 } 481 player.sourceChanged(); 482 player.currentPositionChanged(player.trackPosition, player.trackDuration); 483 } 484 485 function handleZPPlayModeChanged() { 486 player.repeat = zone.handle.playMode === "REPEAT_ALL" || zone.handle.playMode === "REPEAT_ONE" || zone.handle.playMode === "SHUFFLE" ? true : false; 487 player.shuffle = zone.handle.playMode === "SHUFFLE" || zone.handle.playMode === "SHUFFLE_NOREPEAT" ? true : false; 488 } 489 490 function handleZPRenderingGroupChanged() { 491 player.volumeMaster = zone.handle.volumeMaster; 492 player.treble = zone.handle.treble; 493 player.bass = zone.handle.bass; 494 player.mute = zone.handle.muteMaster; 495 player.nightmodeEnabled = zone.handle.nightmode; 496 player.loudnessEnabled = zone.handle.loudness; 497 player.outputFixed = zone.handle.outputFixed; 498 } 499 500 function handleZPRenderingChanged() { 501 renderingControlChanged(); 502 } 503 504 function handleZPRenderingCountChanged() { 505 player.renderingControlCount = zone.handle.renderingCount; 506 } 507 508 function handleZPPlaybackStateChanged() { 509 player.playbackState = zone.handle.playbackState; 510 if (zone.handle.playbackState === "PLAYING" && zone.handle.currentIndex >= 0 && 511 zone.handle.currentTrackDuration > 0) { 512 // Starting playback of queued track the position can be resetted without any event 513 // from the SONOS device. This hack query to retrieve the real position after a short 514 // time (3sec). 515 delaySyncTrackPosition.start(); 516 } else if (zone.handle.playbackState === "STOPPED") { 517 stopped(); 518 } 519 } 520 521 Timer { 522 id: delaySyncTrackPosition 523 interval: 3000 524 onTriggered: { 525 if (isPlaying && trackDuration > 0) { 526 syncTrackPosition(); 527 } 528 } 529 } 530 531 Connections { 532 target: AllZonesModel 533 function onZpConnectedChanged(pid) { if (pid === zone.pid) handleZPConnectedChanged(); } 534 function onZpSourceChanged(pid) { if (pid === zone.pid) handleZPSourceChanged(); } 535 function onZpRenderingCountChanged(pid) { if (pid === zone.pid) handleZPRenderingCountChanged(); } 536 function onZpRenderingGroupChanged(pid) { if (pid === zone.pid) handleZPRenderingGroupChanged(); } 537 function onZpRenderingChanged(pid) { if (pid === zone.pid) handleZPRenderingChanged(); } 538 function onZpPlayModeChanged(pid) { if (pid === zone.pid) handleZPPlayModeChanged(); } 539 function onZpPlaybackStateChanged(pid) { if (pid === zone.pid) handleZPPlaybackStateChanged(); } 540 function onZpSleepTimerChanged(pid) { 541 if (pid === zone.pid) { 542 player.remainingSleepTimerDuration(function(result) { 543 player.sleepTimerEnabled = result > 0 ? true : false 544 }); 545 } 546 } 547 } 548 549 QtObject { 550 id: zone 551 objectName: "playerZone" 552 property int pid: 0 553 property ZonePlayer handle: ZonePlayer { } 554 } 555 556 Loader { 557 id: renderingModelLoader 558 asynchronous: false 559 sourceComponent: Component { 560 RenderingModel { 561 } 562 } 563 } 564 565 // the track queue related to the player 566 // it must be plugged to the current instance of the zone player 567 // also it must be reloaded after any connection event 568 Loader { 569 id: trackQueueLoader 570 asynchronous: false 571 sourceComponent: Component { 572 TrackQueue { 573 // init is done by handleZPConnectedChanged 574 onTrackCountChanged: player.currentCount = trackCount 575 } 576 } 577 } 578 579 Timer { 580 id: playingTimer 581 interval: 1000; 582 running: (player.isPlaying && player.trackDuration > 1) 583 repeat: true; 584 onTriggered: { 585 var npos = player.trackPosition + interval; 586 player.trackPosition = npos > player.trackDuration ? 0 : npos; 587 player.currentPositionChanged(player.trackPosition, player.trackDuration); 588 } 589 } 590} 591