@applicaster/quick-brick-player 16.0.0-rc.100 → 16.0.0-rc.101
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/quick-brick-player",
|
|
3
|
-
"version": "16.0.0-rc.
|
|
3
|
+
"version": "16.0.0-rc.101",
|
|
4
4
|
"description": "Quick Brick Player",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
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-rc.
|
|
41
|
-
"@applicaster/zapp-react-native-ui-components": "16.0.0-rc.
|
|
42
|
-
"@applicaster/zapp-react-native-utils": "16.0.0-rc.
|
|
40
|
+
"@applicaster/zapp-react-native-tvos-app": "16.0.0-rc.101",
|
|
41
|
+
"@applicaster/zapp-react-native-ui-components": "16.0.0-rc.101",
|
|
42
|
+
"@applicaster/zapp-react-native-utils": "16.0.0-rc.101",
|
|
43
43
|
"query-string": "7.1.3",
|
|
44
44
|
"shaka-player": "4.3.5",
|
|
45
45
|
"typeface-montserrat": "^0.0.54",
|
|
@@ -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 = {
|