2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.upnpcontrol.internal.handler;
15 import static org.eclipse.jdt.annotation.Checks.requireNonNull;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.hamcrest.Matchers.is;
18 import static org.junit.jupiter.api.Assertions.assertNull;
19 import static org.mockito.ArgumentMatchers.*;
20 import static org.mockito.Mockito.*;
21 import static org.openhab.binding.upnpcontrol.internal.UpnpControlBindingConstants.*;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.List;
28 import java.util.concurrent.TimeUnit;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.junit.jupiter.api.AfterEach;
33 import org.junit.jupiter.api.BeforeEach;
34 import org.junit.jupiter.api.Test;
35 import org.mockito.ArgumentCaptor;
36 import org.mockito.Mock;
37 import org.openhab.binding.upnpcontrol.internal.audiosink.UpnpAudioSinkReg;
38 import org.openhab.binding.upnpcontrol.internal.config.UpnpControlRendererConfiguration;
39 import org.openhab.binding.upnpcontrol.internal.queue.UpnpEntry;
40 import org.openhab.binding.upnpcontrol.internal.queue.UpnpEntryQueue;
41 import org.openhab.binding.upnpcontrol.internal.queue.UpnpEntryRes;
42 import org.openhab.binding.upnpcontrol.internal.util.UpnpXMLParser;
43 import org.openhab.core.library.types.DecimalType;
44 import org.openhab.core.library.types.NextPreviousType;
45 import org.openhab.core.library.types.OnOffType;
46 import org.openhab.core.library.types.PercentType;
47 import org.openhab.core.library.types.PlayPauseType;
48 import org.openhab.core.library.types.QuantityType;
49 import org.openhab.core.library.types.StringType;
50 import org.openhab.core.library.unit.Units;
51 import org.openhab.core.thing.Channel;
52 import org.openhab.core.thing.ChannelUID;
53 import org.openhab.core.thing.ThingStatus;
54 import org.openhab.core.thing.ThingUID;
55 import org.openhab.core.thing.binding.builder.ChannelBuilder;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.CommandOption;
58 import org.openhab.core.types.State;
59 import org.openhab.core.types.UnDefType;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
64 * Unit tests for {@link UpnpRendererHandler}.
66 * @author Mark Herwege - Initial contribution
68 @SuppressWarnings({ "null", "unchecked" })
70 public class UpnpRendererHandlerTest extends UpnpHandlerTest {
72 private final Logger logger = LoggerFactory.getLogger(UpnpRendererHandlerTest.class);
74 private static final String THING_TYPE_UID = "upnpcontrol:upnprenderer";
75 private static final String THING_UID = THING_TYPE_UID + ":mockrenderer";
77 private static final String LAST_CHANGE_HEADER = "<Event xmlns=\"urn:schemas-upnp-org:metadata-1-0/AVT/\">"
78 + "<InstanceID val=\"0\">";
79 private static final String LAST_CHANGE_FOOTER = "</InstanceID></Event>";
80 private static final String AV_TRANSPORT_URI = "<AVTransportURI val=\"";
81 private static final String AV_TRANSPORT_URI_METADATA = "<AVTransportURIMetaData val=\"";
82 private static final String CURRENT_TRACK_URI = "<CurrentTrackURI val=\"";
83 private static final String CURRENT_TRACK_METADATA = "<CurrentTrackMetaData val=\"";
84 private static final String TRANSPORT_STATE = "<TransportState val=\"";
85 private static final String CLOSE = "\"/>";
87 protected @Nullable UpnpRendererHandler handler;
89 private @Nullable UpnpEntryQueue upnpEntryQueue;
91 private ChannelUID volumeChannelUID = new ChannelUID(THING_UID + ":" + VOLUME);
92 private Channel volumeChannel = ChannelBuilder.create(volumeChannelUID, "Dimmer").build();
94 private ChannelUID muteChannelUID = new ChannelUID(THING_UID + ":" + MUTE);
95 private Channel muteChannel = ChannelBuilder.create(muteChannelUID, "Switch").build();
97 private ChannelUID stopChannelUID = new ChannelUID(THING_UID + ":" + STOP);
98 private Channel stopChannel = ChannelBuilder.create(stopChannelUID, "Switch").build();
100 private ChannelUID controlChannelUID = new ChannelUID(THING_UID + ":" + CONTROL);
101 private Channel controlChannel = ChannelBuilder.create(controlChannelUID, "Player").build();
103 private ChannelUID repeatChannelUID = new ChannelUID(THING_UID + ":" + REPEAT);
104 private Channel repeatChannel = ChannelBuilder.create(repeatChannelUID, "Switch").build();
106 private ChannelUID shuffleChannelUID = new ChannelUID(THING_UID + ":" + SHUFFLE);
107 private Channel shuffleChannel = ChannelBuilder.create(shuffleChannelUID, "Switch").build();
109 private ChannelUID onlyPlayOneChannelUID = new ChannelUID(THING_UID + ":" + ONLY_PLAY_ONE);
110 private Channel onlyPlayOneChannel = ChannelBuilder.create(onlyPlayOneChannelUID, "Switch").build();
112 private ChannelUID uriChannelUID = new ChannelUID(THING_UID + ":" + URI);
113 private Channel uriChannel = ChannelBuilder.create(uriChannelUID, "String").build();
115 private ChannelUID favoriteSelectChannelUID = new ChannelUID(THING_UID + ":" + FAVORITE_SELECT);
116 private Channel favoriteSelectChannel = ChannelBuilder.create(favoriteSelectChannelUID, "String").build();
118 private ChannelUID favoriteChannelUID = new ChannelUID(THING_UID + ":" + FAVORITE);
119 private Channel favoriteChannel = ChannelBuilder.create(favoriteChannelUID, "String").build();
121 private ChannelUID favoriteActionChannelUID = new ChannelUID(THING_UID + ":" + FAVORITE_ACTION);
122 private Channel favoriteActionChannel = ChannelBuilder.create(favoriteActionChannelUID, "String").build();
124 private ChannelUID playlistSelectChannelUID = new ChannelUID(THING_UID + ":" + PLAYLIST_SELECT);
125 private Channel playlistSelectChannel = ChannelBuilder.create(playlistSelectChannelUID, "String").build();
127 private ChannelUID titleChannelUID = new ChannelUID(THING_UID + ":" + TITLE);
128 private Channel titleChannel = ChannelBuilder.create(titleChannelUID, "String").build();
130 private ChannelUID albumChannelUID = new ChannelUID(THING_UID + ":" + ALBUM);
131 private Channel albumChannel = ChannelBuilder.create(albumChannelUID, "String").build();
133 private ChannelUID albumArtChannelUID = new ChannelUID(THING_UID + ":" + ALBUM_ART);
134 private Channel albumArtChannel = ChannelBuilder.create(albumArtChannelUID, "Image").build();
136 private ChannelUID creatorChannelUID = new ChannelUID(THING_UID + ":" + CREATOR);
137 private Channel creatorChannel = ChannelBuilder.create(creatorChannelUID, "String").build();
139 private ChannelUID artistChannelUID = new ChannelUID(THING_UID + ":" + ARTIST);
140 private Channel artistChannel = ChannelBuilder.create(artistChannelUID, "String").build();
142 private ChannelUID publisherChannelUID = new ChannelUID(THING_UID + ":" + PUBLISHER);
143 private Channel publisherChannel = ChannelBuilder.create(publisherChannelUID, "String").build();
145 private ChannelUID genreChannelUID = new ChannelUID(THING_UID + ":" + GENRE);
146 private Channel genreChannel = ChannelBuilder.create(genreChannelUID, "String").build();
148 private ChannelUID trackNumberChannelUID = new ChannelUID(THING_UID + ":" + TRACK_NUMBER);
149 private Channel trackNumberChannel = ChannelBuilder.create(trackNumberChannelUID, "Number").build();
151 private ChannelUID trackDurationChannelUID = new ChannelUID(THING_UID + ":" + TRACK_DURATION);
152 private Channel trackDurationChannel = ChannelBuilder.create(trackDurationChannelUID, "Number:Time").build();
154 private ChannelUID trackPositionChannelUID = new ChannelUID(THING_UID + ":" + TRACK_POSITION);
155 private Channel trackPositionChannel = ChannelBuilder.create(trackPositionChannelUID, "Number:Time").build();
157 private ChannelUID relTrackPositionChannelUID = new ChannelUID(THING_UID + ":" + REL_TRACK_POSITION);
158 private Channel relTrackPositionChannel = ChannelBuilder.create(relTrackPositionChannelUID, "Dimmer").build();
161 private @Nullable UpnpAudioSinkReg audioSinkReg;
165 public void setUp() {
168 // stub thing methods
169 when(thing.getUID()).thenReturn(new ThingUID("upnpcontrol", "upnprenderer", "mockrenderer"));
170 when(thing.getLabel()).thenReturn("MockRenderer");
171 when(thing.getStatus()).thenReturn(ThingStatus.OFFLINE);
174 when(thing.getChannel(VOLUME)).thenReturn(volumeChannel);
175 when(thing.getChannel(MUTE)).thenReturn(muteChannel);
176 when(thing.getChannel(STOP)).thenReturn(stopChannel);
177 when(thing.getChannel(CONTROL)).thenReturn(controlChannel);
178 when(thing.getChannel(REPEAT)).thenReturn(repeatChannel);
179 when(thing.getChannel(SHUFFLE)).thenReturn(shuffleChannel);
180 when(thing.getChannel(ONLY_PLAY_ONE)).thenReturn(onlyPlayOneChannel);
181 when(thing.getChannel(URI)).thenReturn(uriChannel);
182 when(thing.getChannel(FAVORITE_SELECT)).thenReturn(favoriteSelectChannel);
183 when(thing.getChannel(FAVORITE)).thenReturn(favoriteChannel);
184 when(thing.getChannel(FAVORITE_ACTION)).thenReturn(favoriteActionChannel);
185 when(thing.getChannel(PLAYLIST_SELECT)).thenReturn(playlistSelectChannel);
186 when(thing.getChannel(TITLE)).thenReturn(titleChannel);
187 when(thing.getChannel(ALBUM)).thenReturn(albumChannel);
188 when(thing.getChannel(ALBUM_ART)).thenReturn(albumArtChannel);
189 when(thing.getChannel(CREATOR)).thenReturn(creatorChannel);
190 when(thing.getChannel(ARTIST)).thenReturn(artistChannel);
191 when(thing.getChannel(PUBLISHER)).thenReturn(publisherChannel);
192 when(thing.getChannel(GENRE)).thenReturn(genreChannel);
193 when(thing.getChannel(TRACK_NUMBER)).thenReturn(trackNumberChannel);
194 when(thing.getChannel(TRACK_DURATION)).thenReturn(trackDurationChannel);
195 when(thing.getChannel(TRACK_POSITION)).thenReturn(trackPositionChannel);
196 when(thing.getChannel(REL_TRACK_POSITION)).thenReturn(relTrackPositionChannel);
198 // stub config for initialize
199 when(config.as(UpnpControlRendererConfiguration.class)).thenReturn(new UpnpControlRendererConfiguration());
201 // create a media queue for playing
202 List<UpnpEntry> entries = createUpnpEntries();
203 upnpEntryQueue = new UpnpEntryQueue(entries, "54321");
205 handler = spy(new UpnpRendererHandler(requireNonNull(thing), requireNonNull(upnpIOService),
206 requireNonNull(audioSinkReg), requireNonNull(upnpStateDescriptionProvider),
207 requireNonNull(upnpCommandDescriptionProvider), configuration));
209 initHandler(requireNonNull(handler));
211 handler.initialize();
213 expectLastChangeOnStop(true);
214 expectLastChangeOnPlay(true);
215 expectLastChangeOnPause(true);
218 private List<UpnpEntry> createUpnpEntries() {
219 List<UpnpEntry> entries = new ArrayList<>();
221 List<UpnpEntryRes> resList;
223 resList = new ArrayList<>();
224 res = new UpnpEntryRes("http-get:*:audio/mpeg:*", 8054458L, "10", "http://MediaServerContent_0/1/M0/");
225 res.setRes("http://MediaServerContent_0/1/M0/Test_0.mp3");
227 entry = new UpnpEntry("M0", "M0", "C11", "object.item.audioItem").withTitle("Music_00").withResList(resList)
228 .withAlbum("My Music 0").withCreator("Creator_0").withArtist("Artist_0").withGenre("Morning")
229 .withPublisher("myself 0").withAlbumArtUri("").withTrackNumber(1);
231 resList = new ArrayList<>();
232 res = new UpnpEntryRes("http-get:*:audio/wav:*", 1156598L, "6", "http://MediaServerContent_0/1/M1/");
233 res.setRes("http://MediaServerContent_0/1/M1/Test_1.wav");
235 entry = new UpnpEntry("M1", "M1", "C11", "object.item.audioItem").withTitle("Music_01").withResList(resList)
236 .withAlbum("My Music 0").withCreator("Creator_1").withArtist("Artist_1").withGenre("Morning")
237 .withPublisher("myself 1").withAlbumArtUri("").withTrackNumber(2);
239 resList = new ArrayList<>();
240 res = new UpnpEntryRes("http-get:*:audio/mpeg:*", 1156598L, "40", "http://MediaServerContent_0/1/M2/");
241 res.setRes("http://MediaServerContent_0/1/M2/Test_2.mp3");
243 entry = new UpnpEntry("M2", "M2", "C12", "object.item.audioItem").withTitle("Music_02").withResList(resList)
244 .withAlbum("My Music 2").withCreator("Creator_2").withArtist("Artist_2").withGenre("Evening")
245 .withPublisher("myself 2").withAlbumArtUri("").withTrackNumber(1);
252 public void tearDown() {
259 public void testRegisterQueue() {
260 logger.info("testRegisterQueue");
262 // Register a media queue
263 expectLastChangeOnSetAVTransportURI(true, 0);
264 handler.registerQueue(requireNonNull(upnpEntryQueue));
266 checkInternalState(0, 1, true, false, true, false);
267 checkControlChannel(PlayPauseType.PAUSE);
269 checkMetadataChannels(0);
273 public void testPlayQueue() {
274 logger.info("testPlayQueue");
276 // Register a media queue
277 expectLastChangeOnSetAVTransportURI(true, 0);
278 handler.registerQueue(requireNonNull(upnpEntryQueue));
281 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
283 checkInternalState(0, 1, false, true, false, true);
284 checkControlChannel(PlayPauseType.PLAY);
286 checkMetadataChannels(0);
290 public void testStop() {
291 logger.info("testStop");
293 // Register a media queue
294 expectLastChangeOnSetAVTransportURI(true, 0);
295 handler.registerQueue(requireNonNull(upnpEntryQueue));
298 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
301 handler.handleCommand(stopChannelUID, OnOffType.ON);
303 checkInternalState(0, 1, true, false, false, false);
304 checkControlChannel(PlayPauseType.PAUSE);
306 checkMetadataChannels(0);
310 public void testPause() {
311 logger.info("testPause");
313 // Register a media queue
314 expectLastChangeOnSetAVTransportURI(true, 0);
315 handler.registerQueue(requireNonNull(upnpEntryQueue));
318 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
321 handler.handleCommand(controlChannelUID, PlayPauseType.PAUSE);
323 checkControlChannel(PlayPauseType.PAUSE);
326 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
328 checkControlChannel(PlayPauseType.PLAY);
332 public void testPauseNotSupported() {
333 logger.info("testPauseNotSupported");
335 // Some players don't support pause and just continue playing.
336 // Test if we properly switch back to playing state if no confirmation of pause received.
338 // Register a media queue
339 expectLastChangeOnSetAVTransportURI(true, 0);
340 handler.registerQueue(requireNonNull(upnpEntryQueue));
343 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
346 // Do not receive a PAUSED_PLAYBACK response
347 expectLastChangeOnPause(false);
348 handler.handleCommand(controlChannelUID, PlayPauseType.PAUSE);
350 // Wait long enough for status to turn back to PLAYING.
351 // All timeouts in test are set to 1s.
353 TimeUnit.SECONDS.sleep(1);
354 } catch (InterruptedException ignore) {
357 checkControlChannel(PlayPauseType.PLAY);
361 public void testRegisterQueueWhilePlaying() {
362 logger.info("testRegisterQueueWhilePlaying");
364 // Register a media queue
365 expectLastChangeOnSetAVTransportURI(true, 2);
366 List<UpnpEntry> startList = new ArrayList<UpnpEntry>();
367 startList.add(requireNonNull(upnpEntryQueue.get(2)));
368 UpnpEntryQueue startQueue = new UpnpEntryQueue(startList, "54321");
369 handler.registerQueue(requireNonNull(startQueue));
372 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
374 // Register a new media queue
375 expectLastChangeOnSetAVTransportURI(true, 0);
376 handler.registerQueue(requireNonNull(upnpEntryQueue));
378 checkInternalState(2, 0, false, true, true, true);
379 checkControlChannel(PlayPauseType.PLAY);
380 checkSetURI(null, 0);
381 checkMetadataChannels(2);
385 public void testNext() {
386 logger.info("testNext");
388 testNext(false, false);
392 public void testNextRepeat() {
393 logger.info("testNextRepeat");
395 testNext(false, true);
399 public void testNextWhilePlaying() {
400 logger.info("testNextWhilePlaying");
402 testNext(true, false);
406 public void testNextWhilePlayingRepeat() {
407 logger.info("testNextWhilePlayingRepeat");
409 testNext(true, true);
412 private void testNext(boolean play, boolean repeat) {
413 // Register a media queue
414 expectLastChangeOnSetAVTransportURI(true, 0);
415 handler.registerQueue(requireNonNull(upnpEntryQueue));
418 handler.handleCommand(repeatChannelUID, OnOffType.ON);
423 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
427 expectLastChangeOnSetAVTransportURI(true, 1);
428 handler.handleCommand(controlChannelUID, NextPreviousType.NEXT);
430 checkInternalState(1, 2, play ? false : true, play ? true : false, play ? false : true, play ? true : false);
431 checkControlChannel(play ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
433 checkMetadataChannels(1);
436 expectLastChangeOnSetAVTransportURI(true, 2);
437 handler.handleCommand(controlChannelUID, NextPreviousType.NEXT);
439 checkInternalState(2, repeat ? 0 : null, play ? false : true, play ? true : false, play ? false : true,
440 play ? true : false);
441 checkControlChannel(play ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
442 checkSetURI(2, repeat ? 0 : null);
443 checkMetadataChannels(2);
446 expectLastChangeOnSetAVTransportURI(true, 0);
447 handler.handleCommand(controlChannelUID, NextPreviousType.NEXT);
449 checkInternalState(0, 1, (play && repeat) ? false : true, (play && repeat) ? true : false,
450 (play && repeat) ? false : true, (play && repeat) ? true : false);
451 checkControlChannel((play && repeat) ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
453 checkMetadataChannels(0);
457 public void testPrevious() {
458 logger.info("testPrevious");
460 testPrevious(false, false);
464 public void testPreviousRepeat() {
465 logger.info("testPreviousRepeat");
467 testPrevious(false, true);
471 public void testPreviousWhilePlaying() {
472 logger.info("testPreviousWhilePlaying");
474 testPrevious(true, false);
478 public void testPreviousWhilePlayingRepeat() {
479 logger.info("testPreviousWhilePlayingRepeat");
481 testPrevious(true, true);
484 public void testPrevious(boolean play, boolean repeat) {
485 // Register a media queue
486 expectLastChangeOnSetAVTransportURI(true, 0);
487 handler.registerQueue(requireNonNull(upnpEntryQueue));
490 handler.handleCommand(repeatChannelUID, OnOffType.ON);
495 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
499 expectLastChangeOnSetAVTransportURI(true, 1);
500 handler.handleCommand(controlChannelUID, NextPreviousType.NEXT);
503 expectLastChangeOnSetAVTransportURI(true, 2);
504 handler.handleCommand(controlChannelUID, NextPreviousType.PREVIOUS);
506 checkInternalState(0, 1, play ? false : true, play ? true : false, play ? false : true, play ? true : false);
507 checkControlChannel(play ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
509 checkMetadataChannels(0);
512 expectLastChangeOnSetAVTransportURI(true, 0);
513 handler.handleCommand(controlChannelUID, NextPreviousType.PREVIOUS);
515 checkInternalState(repeat ? 2 : 0, repeat ? 0 : 1, (play && repeat) ? false : true,
516 (play && repeat) ? true : false, (play && repeat) ? false : true, (play && repeat) ? true : false);
517 checkControlChannel((play && repeat) ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
518 checkSetURI(repeat ? 2 : 0, repeat ? 0 : 1);
519 checkMetadataChannels(repeat ? 2 : 0);
523 public void testAutoPlayNextInQueue() {
524 logger.info("testAutoPlayNextInQueue");
526 // Register a media queue
527 expectLastChangeOnSetAVTransportURI(true, 0);
528 handler.registerQueue(requireNonNull(upnpEntryQueue));
531 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
533 // We expect GENA LastChange event with new metadata when the renderer starts to play next entry
534 expectLastChangeOnSetAVTransportURI(true, 1);
536 // At the end of the media, we will get GENA LastChange STOP event, renderer should move to next media and play
537 // Force this STOP event for test
538 String lastChange = LAST_CHANGE_HEADER + TRANSPORT_STATE + "STOPPED" + CLOSE + LAST_CHANGE_FOOTER;
539 handler.onValueReceived("LastChange", lastChange, "AVTransport");
541 checkInternalState(1, 2, false, true, false, true);
542 checkControlChannel(PlayPauseType.PLAY);
544 checkMetadataChannels(1);
548 public void testAutoPlayNextInQueueGapless() {
549 logger.info("testAutoPlayNextInQueueGapless");
551 // Register a media queue
552 expectLastChangeOnSetAVTransportURI(true, 0);
553 handler.registerQueue(requireNonNull(upnpEntryQueue));
556 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
558 // We expect GENA LastChange event with new metadata when the renderer starts to play next entry
559 expectLastChangeOnSetAVTransportURI(true, 1);
561 // At the end of the media, we will get GENA event with new URI and metadata
562 String lastChange = LAST_CHANGE_HEADER + AV_TRANSPORT_URI + upnpEntryQueue.get(1).getRes() + CLOSE
563 + AV_TRANSPORT_URI_METADATA + UpnpXMLParser.compileMetadataString(requireNonNull(upnpEntryQueue.get(0)))
564 + CLOSE + CURRENT_TRACK_URI + upnpEntryQueue.get(1).getRes() + CLOSE + CURRENT_TRACK_METADATA
565 + UpnpXMLParser.compileMetadataString(requireNonNull(upnpEntryQueue.get(1))) + CLOSE
566 + LAST_CHANGE_FOOTER;
567 handler.onValueReceived("LastChange", lastChange, "AVTransport");
569 checkInternalState(1, 2, false, true, false, true);
570 checkControlChannel(PlayPauseType.PLAY);
571 checkSetURI(null, 2);
572 checkMetadataChannels(1);
576 public void testOnlyPlayOne() {
577 logger.info("testOnlyPlayOne");
579 handler.handleCommand(onlyPlayOneChannelUID, OnOffType.ON);
581 // Register a media queue
582 expectLastChangeOnSetAVTransportURI(true, 0);
583 handler.registerQueue(requireNonNull(upnpEntryQueue));
586 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
588 checkInternalState(0, 1, false, true, false, true);
589 checkSetURI(0, null);
590 checkMetadataChannels(0);
592 // We expect GENA LastChange event with new metadata when the renderer has finished playing
593 expectLastChangeOnSetAVTransportURI(true, 1);
595 // At the end of the media, we will get GENA LastChange STOP event, renderer should stop
596 // Force this STOP event for test
597 String lastChange = LAST_CHANGE_HEADER + TRANSPORT_STATE + "STOPPED" + CLOSE + LAST_CHANGE_FOOTER;
598 handler.onValueReceived("LastChange", lastChange, "AVTransport");
600 checkInternalState(1, 2, false, false, false, true);
601 checkControlChannel(PlayPauseType.PAUSE);
602 checkSetURI(1, null);
603 checkMetadataChannels(1);
607 public void testPlayUri() {
608 logger.info("testPlayUri");
610 expectLastChangeOnSetAVTransportURI(true, false, 0);
611 handler.handleCommand(uriChannelUID, StringType.valueOf(upnpEntryQueue.get(0).getRes()));
614 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
616 checkInternalState(null, null, false, true, false, false);
617 checkControlChannel(PlayPauseType.PLAY);
618 checkSetURI(0, null, false);
619 checkMetadataChannels(0, true);
623 public void testPlayAction() {
624 logger.info("testPlayAction");
626 expectLastChangeOnSetAVTransportURI(true, false, 0);
628 // Methods called in sequence by audio sink
629 handler.setCurrentURI(upnpEntryQueue.get(0).getRes(), "");
632 checkInternalState(null, null, false, true, false, false);
633 checkControlChannel(PlayPauseType.PLAY);
634 checkSetURI(0, null, false);
635 checkMetadataChannels(0, true);
639 public void testPlayNotification() {
640 logger.info("testPlayNotification");
642 // Register a media queue
643 expectLastChangeOnSetAVTransportURI(true, 0);
644 handler.registerQueue(requireNonNull(upnpEntryQueue));
647 expectLastChangeOnSetVolume(true, 50);
648 handler.setVolume(new PercentType(50));
650 checkInternalState(0, 1, true, false, true, false);
651 checkSetURI(0, 1, true);
652 checkMetadataChannels(0, false);
654 // Play notification, at standard 10% volume above current volume level
655 expectLastChangeOnSetAVTransportURI(true, false, 2);
656 expectLastChangeOnGetPositionInfo(true, "00:00:00");
657 handler.playNotification(upnpEntryQueue.get(2).getRes());
659 checkInternalState(0, 1, true, false, true, false);
660 checkSetURI(2, null, false);
661 checkMetadataChannels(0, false);
662 verify(handler).setVolume(new PercentType(55));
664 // At the end of the notification, we will get GENA LastChange STOP event
665 // Force this STOP event for test
666 expectLastChangeOnSetAVTransportURI(true, false, 0);
667 String lastChange = LAST_CHANGE_HEADER + TRANSPORT_STATE + "STOPPED" + CLOSE + LAST_CHANGE_FOOTER;
668 handler.onValueReceived("LastChange", lastChange, "AVTransport");
670 checkInternalState(0, 1, true, false, true, false);
671 checkMetadataChannels(0, false);
672 verify(handler, times(2)).setVolume(new PercentType(50));
674 // Play media and move to position
675 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
677 checkInternalState(0, 1, false, true, false, true); //
678 checkSetURI(0, 1, true);
679 checkMetadataChannels(0, false);
681 // Play notification again, while simulating the current playing media is at 10s position
682 // Play at volume level provided by audiSink action
683 expectLastChangeOnSetAVTransportURI(true, false, 2);
684 expectLastChangeOnGetPositionInfo(true, "00:00:10");
685 handler.setNotificationVolume(new PercentType(70));
686 handler.playNotification(upnpEntryQueue.get(2).getRes());
688 checkInternalState(0, 1, false, true, false, true);
689 checkSetURI(2, null, false);
690 checkMetadataChannels(0, false);
691 verify(handler).setVolume(new PercentType(70));
693 // Wait long enough for max notification duration to be reached.
694 // In the test, we have enforced 500ms delay through schedule mock.
695 expectLastChangeOnSetAVTransportURI(true, false, 0);
697 TimeUnit.SECONDS.sleep(1);
698 logger.info("Test playing {}, stopped {}", handler.playing, handler.playerStopped);
699 } catch (InterruptedException ignore) {
702 checkInternalState(0, 1, false, true, false, true);
703 checkSetURI(0, null, false);
704 checkMetadataChannels(0, false);
705 verify(handler, times(3)).setVolume(new PercentType(50));
706 verify(callback, times(2)).stateUpdated(trackPositionChannelUID, new QuantityType<>(10, Units.SECOND));
710 public void testFavorite() {
711 logger.info("testFavorite");
713 // Check already called in initialize
714 verify(handler).updateFavoritesList();
717 expectLastChangeOnSetAVTransportURI(true, false, 0);
718 handler.handleCommand(uriChannelUID, StringType.valueOf(upnpEntryQueue.get(0).getRes()));
721 handler.handleCommand(favoriteChannelUID, StringType.valueOf("Test_Favorite"));
722 handler.handleCommand(favoriteActionChannelUID, StringType.valueOf("SAVE"));
724 // Check called after saving favorite
725 verify(handler, times(2)).updateFavoritesList();
727 // Check that FAVORITE_SELECT channel now has the favorite as a state option
728 ArgumentCaptor<List<CommandOption>> commandOptionListCaptor = ArgumentCaptor.forClass(List.class);
729 verify(handler, atLeastOnce()).updateCommandDescription(eq(thing.getChannel(FAVORITE_SELECT).getUID()),
730 commandOptionListCaptor.capture());
731 assertThat(commandOptionListCaptor.getValue().size(), is(1));
732 assertThat(commandOptionListCaptor.getValue().get(0).getCommand(), is("Test_Favorite"));
733 assertThat(commandOptionListCaptor.getValue().get(0).getLabel(), is("Test_Favorite"));
735 // Clear FAVORITE channel
736 handler.handleCommand(favoriteChannelUID, StringType.valueOf(""));
739 expectLastChangeOnSetAVTransportURI(true, false, 2);
740 handler.handleCommand(uriChannelUID, StringType.valueOf(upnpEntryQueue.get(2).getRes()));
742 checkInternalState(null, null, false, true, false, false);
743 checkSetURI(2, null, false);
744 checkMetadataChannels(2, true);
747 expectLastChangeOnSetAVTransportURI(true, false, 0);
748 handler.handleCommand(favoriteSelectChannelUID, StringType.valueOf("Test_Favorite"));
750 checkInternalState(null, null, false, true, false, false);
751 checkControlChannel(PlayPauseType.PLAY);
752 checkSetURI(0, null, false);
753 checkMetadataChannels(0, true);
756 handler.handleCommand(favoriteSelectChannelUID, StringType.valueOf("Test_Favorite"));
757 handler.handleCommand(favoriteActionChannelUID, StringType.valueOf("DELETE"));
759 // Check called after deleting favorite
760 verify(handler, times(3)).updateFavoritesList();
762 // Check that FAVORITE_SELECT channel option list is empty again
763 commandOptionListCaptor = ArgumentCaptor.forClass(List.class);
764 verify(handler, atLeastOnce()).updateCommandDescription(eq(thing.getChannel(FAVORITE_SELECT).getUID()),
765 commandOptionListCaptor.capture());
766 assertThat(commandOptionListCaptor.getValue().size(), is(0));
769 private void expectLastChangeOnStop(boolean respond) {
770 String value = LAST_CHANGE_HEADER + TRANSPORT_STATE + "STOPPED" + CLOSE + LAST_CHANGE_FOOTER;
771 doAnswer(invocation -> {
773 handler.onValueReceived("LastChange", value, "AVTransport");
775 return Collections.emptyMap();
776 }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("Stop"), anyMap());
779 private void expectLastChangeOnPlay(boolean respond) {
780 String value = LAST_CHANGE_HEADER + TRANSPORT_STATE + "PLAYING" + CLOSE + LAST_CHANGE_FOOTER;
781 doAnswer(invocation -> {
783 handler.onValueReceived("LastChange", value, "AVTransport");
785 return Collections.emptyMap();
786 }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("Play"), anyMap());
789 private void expectLastChangeOnPause(boolean respond) {
790 String value = LAST_CHANGE_HEADER + TRANSPORT_STATE + "PAUSED_PLAYBACK" + CLOSE + LAST_CHANGE_FOOTER;
791 doAnswer(invocation -> {
793 handler.onValueReceived("LastChange", value, "AVTransport");
795 return Collections.emptyMap();
796 }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("Pause"), anyMap());
799 private void expectLastChangeOnSetVolume(boolean respond, long volume) {
800 Map<String, String> inputs = new HashMap<>();
801 inputs.put("InstanceID", "0");
802 inputs.put("Channel", UPNP_MASTER);
803 inputs.put("DesiredVolume", String.valueOf(volume));
804 doAnswer(invocation -> {
806 handler.onValueReceived(UPNP_MASTER + "Volume", String.valueOf(volume), "RenderingControl");
808 return Collections.emptyMap();
809 }).when(upnpIOService).invokeAction(eq(handler), eq("RenderingControl"), eq("SetVolume"), eq(inputs));
812 private void expectLastChangeOnGetPositionInfo(boolean respond, String seekTarget) {
813 Map<String, String> inputs = new HashMap<>();
814 inputs.put("InstanceID", "0");
815 doAnswer(invocation -> {
817 handler.onValueReceived("RelTime", seekTarget, "AVTransport");
819 return Collections.emptyMap();
820 }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("GetPositionInfo"), eq(inputs));
823 private void expectLastChangeOnSetAVTransportURI(boolean respond, int mediaId) {
824 expectLastChangeOnSetAVTransportURI(respond, true, mediaId);
827 private void expectLastChangeOnSetAVTransportURI(boolean respond, boolean withMetadata, int mediaId) {
828 String uri = upnpEntryQueue.get(mediaId).getRes();
829 String metadata = UpnpXMLParser.compileMetadataString(requireNonNull(upnpEntryQueue.get(mediaId)));
830 Map<String, String> inputs = new HashMap<>();
831 inputs.put("InstanceID", "0");
832 inputs.put("CurrentURI", uri);
833 inputs.put("CurrentURIMetaData", withMetadata ? metadata : "");
834 String value = LAST_CHANGE_HEADER + AV_TRANSPORT_URI + uri + CLOSE + AV_TRANSPORT_URI_METADATA + metadata
835 + CLOSE + CURRENT_TRACK_URI + uri + CLOSE + CURRENT_TRACK_METADATA + metadata + CLOSE
836 + LAST_CHANGE_FOOTER;
837 doAnswer(invocation -> {
839 handler.onValueReceived("LastChange", value, "AVTransport");
841 return Collections.emptyMap();
842 }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("SetAVTransportURI"), eq(inputs));
845 private void checkInternalState(@Nullable Integer currentEntry, @Nullable Integer nextEntry, boolean playerStopped,
846 boolean playing, boolean registeredQueue, boolean playingQueue) {
847 if (currentEntry == null) {
848 assertNull(handler.currentEntry);
850 assertThat(handler.currentEntry, is(upnpEntryQueue.get(currentEntry)));
852 if (nextEntry == null) {
853 assertNull(handler.nextEntry);
855 assertThat(handler.nextEntry, is(upnpEntryQueue.get(nextEntry)));
857 assertThat(handler.playerStopped, is(playerStopped));
858 assertThat(handler.playing, is(playing));
859 assertThat(handler.registeredQueue, is(registeredQueue));
860 assertThat(handler.playingQueue, is(playingQueue));
863 private void checkControlChannel(Command command) {
864 ArgumentCaptor<PlayPauseType> captor = ArgumentCaptor.forClass(PlayPauseType.class);
865 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CONTROL).getUID()), captor.capture());
866 assertThat(captor.getValue(), is(command));
869 private void checkSetURI(@Nullable Integer current, @Nullable Integer next) {
870 checkSetURI(current, next, true);
873 private void checkSetURI(@Nullable Integer current, @Nullable Integer next, boolean withMetadata) {
874 ArgumentCaptor<String> uriCaptor = ArgumentCaptor.forClass(String.class);
875 ArgumentCaptor<String> metadataCaptor = ArgumentCaptor.forClass(String.class);
876 if (current != null) {
877 verify(handler, atLeastOnce()).setCurrentURI(uriCaptor.capture(), metadataCaptor.capture());
878 assertThat(uriCaptor.getValue(), is(upnpEntryQueue.get(current).getRes()));
880 assertThat(metadataCaptor.getValue(),
881 is(UpnpXMLParser.compileMetadataString(requireNonNull(upnpEntryQueue.get(current)))));
885 verify(handler, atLeastOnce()).setNextURI(uriCaptor.capture(), metadataCaptor.capture());
886 assertThat(uriCaptor.getValue(), is(upnpEntryQueue.get(next).getRes()));
888 assertThat(metadataCaptor.getValue(),
889 is(UpnpXMLParser.compileMetadataString(requireNonNull(upnpEntryQueue.get(next)))));
894 private void checkMetadataChannels(int mediaId) {
895 checkMetadataChannels(mediaId, false);
898 private void checkMetadataChannels(int mediaId, boolean cleared) {
899 ArgumentCaptor<State> stateCaptor = ArgumentCaptor.forClass(State.class);
901 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(URI).getUID()), stateCaptor.capture());
902 assertThat(stateCaptor.getValue(), is(StringType.valueOf(upnpEntryQueue.get(mediaId).getRes())));
904 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(TITLE).getUID()), stateCaptor.capture());
905 assertThat(stateCaptor.getValue(),
906 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getTitle())));
907 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(ALBUM).getUID()), stateCaptor.capture());
908 assertThat(stateCaptor.getValue(),
909 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getAlbum())));
910 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CREATOR).getUID()), stateCaptor.capture());
911 assertThat(stateCaptor.getValue(),
912 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getCreator())));
913 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(ARTIST).getUID()), stateCaptor.capture());
914 assertThat(stateCaptor.getValue(),
915 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getArtist())));
916 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(PUBLISHER).getUID()), stateCaptor.capture());
917 assertThat(stateCaptor.getValue(),
918 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getPublisher())));
919 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(GENRE).getUID()), stateCaptor.capture());
920 assertThat(stateCaptor.getValue(),
921 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getGenre())));
922 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(TRACK_NUMBER).getUID()),
923 stateCaptor.capture());
924 Integer originalTrackNumber = upnpEntryQueue.get(mediaId).getOriginalTrackNumber();
925 if (originalTrackNumber != null) {
926 assertThat(stateCaptor.getValue(), is(cleared ? UnDefType.UNDEF : new DecimalType(originalTrackNumber)));
927 is(new DecimalType(originalTrackNumber));