@applicaster/quick-brick-player 16.0.0-alpha.1334507601 → 16.0.0-alpha.1444784746
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/Player/AudioLayer/PlaylistNavigationController.ts +17 -44
- package/src/Player/AudioLayer/__tests__/PlaylistNavigationController.test.ts +17 -18
- package/src/Player/AudioLayer/__tests__/usePlaylistNavigation.test.ts +153 -1
- package/src/Player/AudioLayer/usePlaylistNavigation.ts +72 -0
- package/src/Player/index.tsx +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/quick-brick-player",
|
|
3
|
-
"version": "16.0.0-alpha.
|
|
3
|
+
"version": "16.0.0-alpha.1444784746",
|
|
4
4
|
"description": "Quick Brick Player",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
},
|
|
36
36
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@applicaster/quick-brick-mobile-transport-controls": "16.0.0-rc.
|
|
38
|
+
"@applicaster/quick-brick-mobile-transport-controls": "16.0.0-rc.93",
|
|
39
39
|
"@applicaster/quick-brick-tv-transport-controls": "16.0.0-rc.69",
|
|
40
|
-
"@applicaster/zapp-react-native-tvos-app": "16.0.0-alpha.
|
|
41
|
-
"@applicaster/zapp-react-native-ui-components": "16.0.0-alpha.
|
|
42
|
-
"@applicaster/zapp-react-native-utils": "16.0.0-alpha.
|
|
40
|
+
"@applicaster/zapp-react-native-tvos-app": "16.0.0-alpha.1444784746",
|
|
41
|
+
"@applicaster/zapp-react-native-ui-components": "16.0.0-alpha.1444784746",
|
|
42
|
+
"@applicaster/zapp-react-native-utils": "16.0.0-alpha.1444784746",
|
|
43
43
|
"query-string": "7.1.3",
|
|
44
44
|
"shaka-player": "4.3.5",
|
|
45
45
|
"typeface-montserrat": "^0.0.54",
|
|
@@ -129,7 +129,17 @@ export class PlaylistNavigationController {
|
|
|
129
129
|
try {
|
|
130
130
|
const plugins = appStore?.get?.("plugins") || [];
|
|
131
131
|
const queuePlugin = findPluginByIdentifier("queue-action", plugins, true);
|
|
132
|
-
const
|
|
132
|
+
const module = (queuePlugin as any)?.module;
|
|
133
|
+
const QueueManager = module?.QueueManager;
|
|
134
|
+
const setupQuickBrickQueue = module?.setupQuickBrickQueue;
|
|
135
|
+
|
|
136
|
+
if (
|
|
137
|
+
QueueManager &&
|
|
138
|
+
!QueueManager.isInitialized?.() &&
|
|
139
|
+
typeof setupQuickBrickQueue === "function"
|
|
140
|
+
) {
|
|
141
|
+
setupQuickBrickQueue();
|
|
142
|
+
}
|
|
133
143
|
|
|
134
144
|
if (QueueManager?.isInitialized?.()) {
|
|
135
145
|
return QueueManager.instance;
|
|
@@ -192,19 +202,15 @@ export class PlaylistNavigationController {
|
|
|
192
202
|
return;
|
|
193
203
|
}
|
|
194
204
|
|
|
195
|
-
const isPlayingFromQueue = Boolean(this.entry?.extensions?.queueItemId);
|
|
196
205
|
const staticNextUrl = this.entry?.extensions?.play_next_feed_url;
|
|
197
206
|
const staticPrevUrl = this.entry?.extensions?.play_previous_feed_url;
|
|
198
207
|
|
|
199
|
-
const hasNext =
|
|
200
|
-
? upcoming.length > 0 || Boolean(staticNextUrl)
|
|
201
|
-
: Boolean(staticNextUrl) || upcoming.length > 0;
|
|
208
|
+
const hasNext = upcoming.length > 0 || Boolean(staticNextUrl);
|
|
202
209
|
|
|
203
210
|
const canRestart = (this.playerState?.seekableDuration ?? 0) > 0;
|
|
204
211
|
|
|
205
|
-
const hasPrevious =
|
|
206
|
-
|
|
207
|
-
: Boolean(staticPrevUrl) || history.length > 0 || canRestart;
|
|
212
|
+
const hasPrevious =
|
|
213
|
+
history.length > 0 || Boolean(staticPrevUrl) || canRestart;
|
|
208
214
|
|
|
209
215
|
if (this.hasNext$.getValue() !== hasNext) {
|
|
210
216
|
this.hasNext$.next(hasNext);
|
|
@@ -227,9 +233,7 @@ export class PlaylistNavigationController {
|
|
|
227
233
|
|
|
228
234
|
if (this.isDestroyed) return;
|
|
229
235
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (isPlayingFromQueue && upcoming.length > 0) {
|
|
236
|
+
if (upcoming.length > 0) {
|
|
233
237
|
const nextItem = upcoming[0];
|
|
234
238
|
|
|
235
239
|
log_info(
|
|
@@ -259,20 +263,6 @@ export class PlaylistNavigationController {
|
|
|
259
263
|
);
|
|
260
264
|
|
|
261
265
|
this.navigator?.replaceTop?.(target);
|
|
262
|
-
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (upcoming.length > 0) {
|
|
267
|
-
const nextItem = upcoming[0];
|
|
268
|
-
|
|
269
|
-
log_info(
|
|
270
|
-
`goNext: playing next item from queue "${nextItem.title}", id: ${nextItem.id}`,
|
|
271
|
-
{ targetId: nextItem.id }
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
if (this.isDestroyed) return;
|
|
275
|
-
this.navigator?.replaceTop?.(nextItem);
|
|
276
266
|
}
|
|
277
267
|
} catch (error) {
|
|
278
268
|
log_error("goNext: could not navigate to next entry", { error });
|
|
@@ -293,7 +283,6 @@ export class PlaylistNavigationController {
|
|
|
293
283
|
|
|
294
284
|
if (this.isDestroyed) return;
|
|
295
285
|
|
|
296
|
-
const isPlayingFromQueue = Boolean(this.entry?.extensions?.queueItemId);
|
|
297
286
|
const staticPrevUrl = this.entry?.extensions?.play_previous_feed_url;
|
|
298
287
|
|
|
299
288
|
const currentTime = this.playerState?.currentTime ?? 0;
|
|
@@ -302,9 +291,7 @@ export class PlaylistNavigationController {
|
|
|
302
291
|
const hasQueueHistory = history.length > 0;
|
|
303
292
|
const hasStaticPrev = Boolean(staticPrevUrl);
|
|
304
293
|
|
|
305
|
-
const hasTarget =
|
|
306
|
-
? hasQueueHistory || hasStaticPrev
|
|
307
|
-
: hasStaticPrev || hasQueueHistory;
|
|
294
|
+
const hasTarget = hasQueueHistory || hasStaticPrev;
|
|
308
295
|
|
|
309
296
|
const shouldRestart =
|
|
310
297
|
canRestart && (currentTime > RESTART_THRESHOLD_SECONDS || !hasTarget);
|
|
@@ -316,7 +303,7 @@ export class PlaylistNavigationController {
|
|
|
316
303
|
return;
|
|
317
304
|
}
|
|
318
305
|
|
|
319
|
-
if (
|
|
306
|
+
if (hasQueueHistory) {
|
|
320
307
|
const prevItem = history[history.length - 1];
|
|
321
308
|
|
|
322
309
|
log_info(
|
|
@@ -344,20 +331,6 @@ export class PlaylistNavigationController {
|
|
|
344
331
|
);
|
|
345
332
|
|
|
346
333
|
this.navigator?.replaceTop?.(target);
|
|
347
|
-
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (hasQueueHistory) {
|
|
352
|
-
const prevItem = history[history.length - 1];
|
|
353
|
-
|
|
354
|
-
log_info(
|
|
355
|
-
`goPrevious: playing previous item from queue "${prevItem.title}", id: ${prevItem.id}`,
|
|
356
|
-
{ targetId: prevItem.id }
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
if (this.isDestroyed) return;
|
|
360
|
-
this.navigator?.replaceTop?.(prevItem);
|
|
361
334
|
}
|
|
362
335
|
} catch (error) {
|
|
363
336
|
log_error("goPrevious: could not navigate to previous entry", { error });
|
|
@@ -380,7 +380,7 @@ describe("PlaylistNavigationController", () => {
|
|
|
380
380
|
controller.destroy();
|
|
381
381
|
});
|
|
382
382
|
|
|
383
|
-
it("prioritizes static feed URLs when entry is not from queue", async () => {
|
|
383
|
+
it("prioritizes queue navigation over static feed URLs even when entry is not from queue", async () => {
|
|
384
384
|
const queueNextItem = { id: "queue_next" } as ZappEntry;
|
|
385
385
|
const queuePrevItem = { id: "queue_prev" } as ZappEntry;
|
|
386
386
|
const staticNextTarget = { id: "static_next" } as ZappEntry;
|
|
@@ -411,30 +411,28 @@ describe("PlaylistNavigationController", () => {
|
|
|
411
411
|
});
|
|
412
412
|
|
|
413
413
|
await controller.goNext();
|
|
414
|
-
expect(loadFeedEntryMock).
|
|
415
|
-
expect(navigator.replaceTop).toHaveBeenCalledWith(
|
|
414
|
+
expect(loadFeedEntryMock).not.toHaveBeenCalled();
|
|
415
|
+
expect(navigator.replaceTop).toHaveBeenCalledWith(queueNextItem);
|
|
416
416
|
|
|
417
417
|
await controller.goPrevious();
|
|
418
|
-
expect(loadFeedEntryMock).
|
|
419
|
-
expect(navigator.replaceTop).toHaveBeenCalledWith(
|
|
418
|
+
expect(loadFeedEntryMock).not.toHaveBeenCalled();
|
|
419
|
+
expect(navigator.replaceTop).toHaveBeenCalledWith(queuePrevItem);
|
|
420
420
|
|
|
421
421
|
controller.destroy();
|
|
422
422
|
});
|
|
423
423
|
|
|
424
|
-
it("
|
|
425
|
-
const
|
|
426
|
-
const
|
|
424
|
+
it("falls back to static feed URLs when queue is empty", async () => {
|
|
425
|
+
const staticNextTarget = { id: "static_next" } as ZappEntry;
|
|
426
|
+
const staticPrevTarget = { id: "static_prev" } as ZappEntry;
|
|
427
427
|
|
|
428
|
-
mockQueueManager.instance.getUpcomingQueue.mockResolvedValue([
|
|
429
|
-
|
|
430
|
-
]);
|
|
428
|
+
mockQueueManager.instance.getUpcomingQueue.mockResolvedValue([]);
|
|
429
|
+
mockQueueManager.instance.getHistoryQueue.mockResolvedValue([]);
|
|
431
430
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
431
|
+
loadFeedEntryMock.mockImplementation((url) =>
|
|
432
|
+
Promise.resolve(url === NEXT_URL ? staticNextTarget : staticPrevTarget)
|
|
433
|
+
);
|
|
435
434
|
|
|
436
435
|
const entry = entryWith({
|
|
437
|
-
queueItemId: "current_queue_slot",
|
|
438
436
|
play_next_feed_url: NEXT_URL,
|
|
439
437
|
play_previous_feed_url: PREV_URL,
|
|
440
438
|
});
|
|
@@ -447,11 +445,12 @@ describe("PlaylistNavigationController", () => {
|
|
|
447
445
|
});
|
|
448
446
|
|
|
449
447
|
await controller.goNext();
|
|
450
|
-
expect(loadFeedEntryMock).
|
|
451
|
-
expect(navigator.replaceTop).toHaveBeenCalledWith(
|
|
448
|
+
expect(loadFeedEntryMock).toHaveBeenCalledWith(NEXT_URL, entry);
|
|
449
|
+
expect(navigator.replaceTop).toHaveBeenCalledWith(staticNextTarget);
|
|
452
450
|
|
|
453
451
|
await controller.goPrevious();
|
|
454
|
-
expect(
|
|
452
|
+
expect(loadFeedEntryMock).toHaveBeenCalledWith(PREV_URL, entry);
|
|
453
|
+
expect(navigator.replaceTop).toHaveBeenCalledWith(staticPrevTarget);
|
|
455
454
|
|
|
456
455
|
controller.destroy();
|
|
457
456
|
});
|
|
@@ -2,8 +2,12 @@ import { renderHook, act } from "@testing-library/react-native";
|
|
|
2
2
|
import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
3
3
|
import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
|
|
4
4
|
import { loadFeedEntry } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/OverlayObserver/utils";
|
|
5
|
+
import { RemoteCommand } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/const";
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
getTransportControls,
|
|
9
|
+
usePlaylistNavigation,
|
|
10
|
+
} from "../usePlaylistNavigation";
|
|
7
11
|
|
|
8
12
|
jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => ({
|
|
9
13
|
useNavigation: jest.fn(),
|
|
@@ -24,6 +28,13 @@ jest.mock(
|
|
|
24
28
|
})
|
|
25
29
|
);
|
|
26
30
|
|
|
31
|
+
jest.mock(
|
|
32
|
+
"@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand",
|
|
33
|
+
() => ({
|
|
34
|
+
PlayerNativeCommandTypes: { setPlayerControls: "setPlayerControls" },
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
|
|
27
38
|
const useNavigationMock = useNavigation as jest.Mock;
|
|
28
39
|
const loadFeedEntryMock = loadFeedEntry as jest.Mock;
|
|
29
40
|
const usePlayerMock = usePlayer as jest.Mock;
|
|
@@ -384,4 +395,145 @@ describe("usePlaylistNavigation", () => {
|
|
|
384
395
|
expect(loadFeedEntryMock).not.toHaveBeenCalled();
|
|
385
396
|
});
|
|
386
397
|
});
|
|
398
|
+
|
|
399
|
+
describe("remote commands", () => {
|
|
400
|
+
let listeners: Record<string, QuickBrickPlayer.SharedPlayerCallBacks>;
|
|
401
|
+
const command = jest.fn();
|
|
402
|
+
|
|
403
|
+
const remote = (id: RemoteCommand) =>
|
|
404
|
+
act(async () => {
|
|
405
|
+
Object.values(listeners).forEach((l) => l.onRemoteCommand?.({ id }));
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
beforeEach(() => {
|
|
409
|
+
listeners = {};
|
|
410
|
+
command.mockClear();
|
|
411
|
+
|
|
412
|
+
usePlayerMock.mockReturnValue({
|
|
413
|
+
playerId: "player-1",
|
|
414
|
+
command,
|
|
415
|
+
seekTo,
|
|
416
|
+
addListener: ({ id, listener }) => {
|
|
417
|
+
listeners[id] = listener;
|
|
418
|
+
|
|
419
|
+
return () => delete listeners[id];
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("plays next when the lock screen / CarPlay next button is pressed", async () => {
|
|
425
|
+
const target = { id: "next" } as ZappEntry;
|
|
426
|
+
loadFeedEntryMock.mockResolvedValue(target);
|
|
427
|
+
|
|
428
|
+
await renderPlaylistNavigation(
|
|
429
|
+
entryWith({ play_next_feed_url: NEXT_URL }),
|
|
430
|
+
stateAt(0)
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
await remote(RemoteCommand.NextTrack);
|
|
434
|
+
|
|
435
|
+
expect(replaceTop).toHaveBeenCalledWith(target);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("restarts the track on remote previous past the threshold", async () => {
|
|
439
|
+
await renderPlaylistNavigation(entryWith({}), stateAt(10));
|
|
440
|
+
|
|
441
|
+
await remote(RemoteCommand.PreviousTrack);
|
|
442
|
+
|
|
443
|
+
expect(seekTo).toHaveBeenCalledWith(0);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("ignores commands native handles itself", async () => {
|
|
447
|
+
await renderPlaylistNavigation(
|
|
448
|
+
entryWith({ play_next_feed_url: NEXT_URL }),
|
|
449
|
+
stateAt(10)
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
await remote(RemoteCommand.Pause);
|
|
453
|
+
|
|
454
|
+
expect(replaceTop).not.toHaveBeenCalled();
|
|
455
|
+
expect(seekTo).not.toHaveBeenCalled();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("sends native the controls snapshot to show", async () => {
|
|
459
|
+
await renderPlaylistNavigation(
|
|
460
|
+
entryWith({ play_next_feed_url: NEXT_URL }),
|
|
461
|
+
stateAt(0, NOT_SEEKABLE)
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
expect(command).toHaveBeenLastCalledWith("setPlayerControls", {
|
|
465
|
+
transport: [
|
|
466
|
+
RemoteCommand.Play,
|
|
467
|
+
RemoteCommand.Pause,
|
|
468
|
+
RemoteCommand.SkipBackward,
|
|
469
|
+
RemoteCommand.NextTrack,
|
|
470
|
+
],
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("stops listening once unmounted", async () => {
|
|
475
|
+
const { unmount } = await renderPlaylistNavigation(
|
|
476
|
+
entryWith({ play_next_feed_url: NEXT_URL }),
|
|
477
|
+
stateAt(0)
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
unmount();
|
|
481
|
+
|
|
482
|
+
expect(listeners).toEqual({});
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
describe("getTransportControls", () => {
|
|
488
|
+
it("falls back to skip backward and leaves next out when there is no playlist", () => {
|
|
489
|
+
expect(
|
|
490
|
+
getTransportControls({
|
|
491
|
+
hasNext: false,
|
|
492
|
+
hasPrevious: false,
|
|
493
|
+
isLive: false,
|
|
494
|
+
})
|
|
495
|
+
).toEqual([
|
|
496
|
+
RemoteCommand.Play,
|
|
497
|
+
RemoteCommand.Pause,
|
|
498
|
+
RemoteCommand.SkipBackward,
|
|
499
|
+
]);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("leaves next out without a next item, so it shows disabled", () => {
|
|
503
|
+
expect(
|
|
504
|
+
getTransportControls({ hasNext: false, hasPrevious: true, isLive: false })
|
|
505
|
+
).toEqual([
|
|
506
|
+
RemoteCommand.Play,
|
|
507
|
+
RemoteCommand.Pause,
|
|
508
|
+
RemoteCommand.PreviousTrack,
|
|
509
|
+
]);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("puts track buttons in place of skip ones", () => {
|
|
513
|
+
expect(
|
|
514
|
+
getTransportControls({ hasNext: true, hasPrevious: true, isLive: false })
|
|
515
|
+
).toEqual([
|
|
516
|
+
RemoteCommand.Play,
|
|
517
|
+
RemoteCommand.Pause,
|
|
518
|
+
RemoteCommand.PreviousTrack,
|
|
519
|
+
RemoteCommand.NextTrack,
|
|
520
|
+
]);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("uses stop and never skips on live", () => {
|
|
524
|
+
expect(
|
|
525
|
+
getTransportControls({ hasNext: false, hasPrevious: false, isLive: true })
|
|
526
|
+
).toEqual([RemoteCommand.Play, RemoteCommand.Stop]);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("keeps track buttons on live, e.g. a list of radio stations", () => {
|
|
530
|
+
expect(
|
|
531
|
+
getTransportControls({ hasNext: true, hasPrevious: true, isLive: true })
|
|
532
|
+
).toEqual([
|
|
533
|
+
RemoteCommand.Play,
|
|
534
|
+
RemoteCommand.Stop,
|
|
535
|
+
RemoteCommand.PreviousTrack,
|
|
536
|
+
RemoteCommand.NextTrack,
|
|
537
|
+
]);
|
|
538
|
+
});
|
|
387
539
|
});
|
|
@@ -2,8 +2,45 @@ import * as React from "react";
|
|
|
2
2
|
|
|
3
3
|
import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
4
4
|
import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
|
|
5
|
+
import { PlayerNativeCommandTypes } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
|
|
6
|
+
import { RemoteCommand } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/const";
|
|
5
7
|
import { PlaylistNavigationController } from "./PlaylistNavigationController";
|
|
6
8
|
|
|
9
|
+
const REMOTE_COMMANDS_LISTENER_ID = "playlist-navigation-remote-commands";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Buttons native should expose on the lock screen / Control Center / CarPlay.
|
|
13
|
+
* Native enables exactly these and disables the rest. A track button takes its
|
|
14
|
+
* slot over the skip one, since the system shows only one of them per side.
|
|
15
|
+
* Without a next item next is left out, so the system shows it disabled.
|
|
16
|
+
*/
|
|
17
|
+
export const getTransportControls = ({
|
|
18
|
+
hasNext,
|
|
19
|
+
hasPrevious,
|
|
20
|
+
isLive,
|
|
21
|
+
}: {
|
|
22
|
+
hasNext: boolean;
|
|
23
|
+
hasPrevious: boolean;
|
|
24
|
+
isLive: boolean;
|
|
25
|
+
}): RemoteCommand[] => {
|
|
26
|
+
const commands = [
|
|
27
|
+
RemoteCommand.Play,
|
|
28
|
+
isLive ? RemoteCommand.Stop : RemoteCommand.Pause,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
if (hasPrevious) {
|
|
32
|
+
commands.push(RemoteCommand.PreviousTrack);
|
|
33
|
+
} else if (!isLive) {
|
|
34
|
+
commands.push(RemoteCommand.SkipBackward);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (hasNext) {
|
|
38
|
+
commands.push(RemoteCommand.NextTrack);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return commands;
|
|
42
|
+
};
|
|
43
|
+
|
|
7
44
|
export type PlaylistNavigation = {
|
|
8
45
|
hasNext: boolean;
|
|
9
46
|
hasPrevious: boolean;
|
|
@@ -57,6 +94,41 @@ export const usePlaylistNavigation = (
|
|
|
57
94
|
};
|
|
58
95
|
}, [controller]);
|
|
59
96
|
|
|
97
|
+
// Lock screen / Control Center / CarPlay next & previous go through the same
|
|
98
|
+
// controller as the on-screen buttons, so JS stays the single source of truth.
|
|
99
|
+
React.useEffect(
|
|
100
|
+
() =>
|
|
101
|
+
player?.addListener?.({
|
|
102
|
+
id: REMOTE_COMMANDS_LISTENER_ID,
|
|
103
|
+
listener: {
|
|
104
|
+
onRemoteCommand: ({ id }) => {
|
|
105
|
+
if (id === RemoteCommand.NextTrack) {
|
|
106
|
+
controller.goNext();
|
|
107
|
+
} else if (id === RemoteCommand.PreviousTrack) {
|
|
108
|
+
controller.goPrevious();
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
[player, controller]
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const isLive = Boolean(playerState?.isLive);
|
|
117
|
+
|
|
118
|
+
// Full snapshot: native renders exactly this and drops the rest. Each key is
|
|
119
|
+
// one kind of buttons, so they stay separate lists rather than one flat list.
|
|
120
|
+
React.useEffect(() => {
|
|
121
|
+
const controls = {
|
|
122
|
+
transport: getTransportControls({ hasNext, hasPrevious, isLive }),
|
|
123
|
+
// TODO: other button kinds go in as their own keys next to `transport`,
|
|
124
|
+
// e.g. custom / native-drawn buttons ("play next", CarPlay / Android Auto
|
|
125
|
+
// row buttons). Add a composer on Player once more than this hook fills it.
|
|
126
|
+
// actions: [],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
player?.command?.(PlayerNativeCommandTypes.setPlayerControls, controls);
|
|
130
|
+
}, [player, hasNext, hasPrevious, isLive]);
|
|
131
|
+
|
|
60
132
|
const goNext = React.useCallback(
|
|
61
133
|
async () => controller.goNext(),
|
|
62
134
|
[controller]
|
package/src/Player/index.tsx
CHANGED
|
@@ -398,6 +398,22 @@ export default class VideoPlayer extends React.Component<
|
|
|
398
398
|
RNPlayerManager?.[this.playerPluginId]?.stopBackgroundPlayback?.();
|
|
399
399
|
};
|
|
400
400
|
|
|
401
|
+
command = (command: string, userInfo?: Record<string, any>) => {
|
|
402
|
+
if (
|
|
403
|
+
this.isReactNativeMethodsSupported() &&
|
|
404
|
+
RNPlayerManager?.[this.playerPluginId]?.command &&
|
|
405
|
+
this._root
|
|
406
|
+
) {
|
|
407
|
+
const node = findNodeHandle(this._root);
|
|
408
|
+
|
|
409
|
+
if (!node) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
RNPlayerManager?.[this.playerPluginId]?.command(node, command, userInfo);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
401
417
|
forward = (seekTime = SEEK_TIME) => {
|
|
402
418
|
const currentTime = this.state.currentTime;
|
|
403
419
|
|
|
@@ -535,6 +551,10 @@ export default class VideoPlayer extends React.Component<
|
|
|
535
551
|
return this.props?.listener?.onError(newError);
|
|
536
552
|
};
|
|
537
553
|
|
|
554
|
+
onRemoteCommand = ({ nativeEvent = null }) => {
|
|
555
|
+
this.props?.listener?.onRemoteCommand?.(nativeEvent);
|
|
556
|
+
};
|
|
557
|
+
|
|
538
558
|
onPlayerPause = ({ nativeEvent = null }) => {
|
|
539
559
|
this.setState({ paused: true });
|
|
540
560
|
this.props?.listener?.onPlayerPause(nativeEvent);
|
|
@@ -1069,6 +1089,7 @@ export default class VideoPlayer extends React.Component<
|
|
|
1069
1089
|
onPlayerResume: this.onPlayerResume,
|
|
1070
1090
|
onPlayerSeekComplete: this.onPlayerSeekComplete,
|
|
1071
1091
|
onPlayerSeekStart: this.onPlayerSeekStart,
|
|
1092
|
+
onRemoteCommand: this.onRemoteCommand,
|
|
1072
1093
|
};
|
|
1073
1094
|
|
|
1074
1095
|
const lifecycleEvents = {
|