@viamrobotics/test-widgets 0.1.5 → 0.1.6
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/dist/builtin.js +41 -32
- package/dist/client-map.d.ts +31 -0
- package/dist/client-map.js +35 -0
- package/dist/components/widgets/camera/camera.svelte +1 -10
- package/dist/components/widgets/camera/live-or-polling-video.svelte +1 -5
- package/dist/components/widgets/camera/live-or-polling-video.svelte.d.ts +0 -1
- package/dist/components/widgets/camera/picture-in-picture-button.svelte +11 -56
- package/dist/components/widgets/camera/picture-in-picture-button.svelte.d.ts +2 -3
- package/dist/components/widgets/do-command/do-command.svelte +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/pip/context.svelte.d.ts +13 -0
- package/dist/pip/context.svelte.js +125 -0
- package/package.json +1 -1
package/dist/builtin.js
CHANGED
|
@@ -1,49 +1,58 @@
|
|
|
1
|
-
import { ArmClient, BaseClient, BoardClient, ButtonClient, CameraClient, DataManagerClient, DiscoveryClient, EncoderClient, GantryClient, GenericComponentClient, GenericServiceClient, GripperClient, InputControllerClient, MLModelClient, MotionClient, MotorClient, MovementSensorClient, NavigationClient, PoseTrackerClient, PowerSensorClient, SensorClient, ServoClient, SlamClient, SwitchClient, VideoClient, VisionClient, WorldStateStoreClient, } from '@viamrobotics/sdk';
|
|
2
1
|
import { ArmWidget, BaseWidget, BoardWidget, ButtonWidget, CameraWidget, DiscoveryWidget, EncoderWidget, GantryWidget, GripperWidget, InputControllerWidget, MLModelServiceWidget, MotorWidget, MovementSensorWidget, NavigationServiceWidget, PowerSensorWidget, SensorWidget, ServoWidget, SlamWidget, SwitchWidget, VisionServiceWidget, } from './';
|
|
2
|
+
import { clientMap } from "./client-map.js";
|
|
3
3
|
import { getResourceAPI } from "./resource.js";
|
|
4
|
+
export { clientForBuiltinResource } from "./client-map.js";
|
|
4
5
|
// The types are nicer to work with as arrays (as opposed to a record of objects) because TS will infer the types.
|
|
5
6
|
// try not to expose this map so that we can easily refactor it.
|
|
6
7
|
const resourceMap =
|
|
7
8
|
// api: [client, testView, showResourceInControlView]
|
|
8
9
|
// list created via `cat rdkbuiltins/viam-server-stable.json | rg "api" | sort | uniq`
|
|
9
10
|
{
|
|
10
|
-
'rdk:component:arm': [
|
|
11
|
-
'rdk:component:base': [
|
|
12
|
-
'rdk:component:board': [
|
|
13
|
-
'rdk:component:button': [
|
|
14
|
-
'rdk:component:camera': [
|
|
15
|
-
'rdk:component:encoder': [
|
|
16
|
-
'rdk:component:gantry': [
|
|
17
|
-
'rdk:component:generic': [
|
|
18
|
-
'rdk:component:gripper': [
|
|
19
|
-
'rdk:component:input_controller': [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'rdk:component:
|
|
25
|
-
'rdk:component:
|
|
26
|
-
|
|
11
|
+
'rdk:component:arm': [clientMap['rdk:component:arm'], ArmWidget, true],
|
|
12
|
+
'rdk:component:base': [clientMap['rdk:component:base'], BaseWidget, true],
|
|
13
|
+
'rdk:component:board': [clientMap['rdk:component:board'], BoardWidget, true],
|
|
14
|
+
'rdk:component:button': [clientMap['rdk:component:button'], ButtonWidget, true],
|
|
15
|
+
'rdk:component:camera': [clientMap['rdk:component:camera'], CameraWidget, true],
|
|
16
|
+
'rdk:component:encoder': [clientMap['rdk:component:encoder'], EncoderWidget, true],
|
|
17
|
+
'rdk:component:gantry': [clientMap['rdk:component:gantry'], GantryWidget, true],
|
|
18
|
+
'rdk:component:generic': [clientMap['rdk:component:generic'], undefined, true],
|
|
19
|
+
'rdk:component:gripper': [clientMap['rdk:component:gripper'], GripperWidget, true],
|
|
20
|
+
'rdk:component:input_controller': [
|
|
21
|
+
clientMap['rdk:component:input_controller'],
|
|
22
|
+
InputControllerWidget,
|
|
23
|
+
true,
|
|
24
|
+
],
|
|
25
|
+
'rdk:component:motor': [clientMap['rdk:component:motor'], MotorWidget, true],
|
|
26
|
+
'rdk:component:movement_sensor': [
|
|
27
|
+
clientMap['rdk:component:movement_sensor'],
|
|
28
|
+
MovementSensorWidget,
|
|
29
|
+
true,
|
|
30
|
+
],
|
|
31
|
+
'rdk:component:pose_tracker': [clientMap['rdk:component:pose_tracker'], undefined, true],
|
|
32
|
+
'rdk:component:power_sensor': [
|
|
33
|
+
clientMap['rdk:component:power_sensor'],
|
|
34
|
+
PowerSensorWidget,
|
|
35
|
+
true,
|
|
36
|
+
],
|
|
37
|
+
'rdk:component:sensor': [clientMap['rdk:component:sensor'], SensorWidget, true],
|
|
38
|
+
'rdk:component:servo': [clientMap['rdk:component:servo'], ServoWidget, true],
|
|
39
|
+
'rdk:component:switch': [clientMap['rdk:component:switch'], SwitchWidget, true],
|
|
27
40
|
'rdk:service:base_remote_control': [undefined, undefined, true],
|
|
28
41
|
// dont show -- confusing to users
|
|
29
|
-
'rdk:service:data_manager': [
|
|
30
|
-
'rdk:service:discovery': [
|
|
31
|
-
'rdk:service:generic': [
|
|
32
|
-
'rdk:service:mlmodel': [
|
|
42
|
+
'rdk:service:data_manager': [clientMap['rdk:service:data_manager'], undefined, false],
|
|
43
|
+
'rdk:service:discovery': [clientMap['rdk:service:discovery'], DiscoveryWidget, true],
|
|
44
|
+
'rdk:service:generic': [clientMap['rdk:service:generic'], undefined, true],
|
|
45
|
+
'rdk:service:mlmodel': [clientMap['rdk:service:mlmodel'], MLModelServiceWidget, true],
|
|
33
46
|
// dont show -- confusing to users
|
|
34
|
-
'rdk:service:motion': [
|
|
35
|
-
'rdk:service:navigation': [
|
|
47
|
+
'rdk:service:motion': [clientMap['rdk:service:motion'], undefined, false],
|
|
48
|
+
'rdk:service:navigation': [clientMap['rdk:service:navigation'], NavigationServiceWidget, true],
|
|
36
49
|
// dont show -- confusing to users
|
|
37
50
|
'rdk:service:sensors': [undefined, undefined, false],
|
|
38
51
|
'rdk:service:shell': [undefined, undefined, false],
|
|
39
|
-
'rdk:service:slam': [
|
|
40
|
-
'rdk:service:vision': [
|
|
41
|
-
'rdk:service:world_state_store': [
|
|
42
|
-
'rdk:service:video': [
|
|
43
|
-
};
|
|
44
|
-
export const clientForBuiltinResource = (resource) => {
|
|
45
|
-
const resAPI = getResourceAPI(resource);
|
|
46
|
-
return resAPI in resourceMap ? resourceMap[resAPI][0] : undefined;
|
|
52
|
+
'rdk:service:slam': [clientMap['rdk:service:slam'], SlamWidget, true],
|
|
53
|
+
'rdk:service:vision': [clientMap['rdk:service:vision'], VisionServiceWidget, true],
|
|
54
|
+
'rdk:service:world_state_store': [clientMap['rdk:service:world_state_store'], undefined, true],
|
|
55
|
+
'rdk:service:video': [clientMap['rdk:service:video'], undefined, true],
|
|
47
56
|
};
|
|
48
57
|
export const viewForBuiltinResource = (resource) => {
|
|
49
58
|
const resAPI = getResourceAPI(resource);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ArmClient, BaseClient, BoardClient, ButtonClient, CameraClient, DataManagerClient, DiscoveryClient, EncoderClient, GantryClient, GenericComponentClient, GenericServiceClient, GripperClient, InputControllerClient, MLModelClient, MotionClient, MotorClient, MovementSensorClient, NavigationClient, PoseTrackerClient, PowerSensorClient, type ResourceName, SensorClient, ServoClient, SlamClient, SwitchClient, VideoClient, VisionClient, WorldStateStoreClient } from '@viamrobotics/sdk';
|
|
2
|
+
export declare const clientMap: {
|
|
3
|
+
readonly 'rdk:component:arm': typeof ArmClient;
|
|
4
|
+
readonly 'rdk:component:base': typeof BaseClient;
|
|
5
|
+
readonly 'rdk:component:board': typeof BoardClient;
|
|
6
|
+
readonly 'rdk:component:button': typeof ButtonClient;
|
|
7
|
+
readonly 'rdk:component:camera': typeof CameraClient;
|
|
8
|
+
readonly 'rdk:component:encoder': typeof EncoderClient;
|
|
9
|
+
readonly 'rdk:component:gantry': typeof GantryClient;
|
|
10
|
+
readonly 'rdk:component:generic': typeof GenericComponentClient;
|
|
11
|
+
readonly 'rdk:component:gripper': typeof GripperClient;
|
|
12
|
+
readonly 'rdk:component:input_controller': typeof InputControllerClient;
|
|
13
|
+
readonly 'rdk:component:motor': typeof MotorClient;
|
|
14
|
+
readonly 'rdk:component:movement_sensor': typeof MovementSensorClient;
|
|
15
|
+
readonly 'rdk:component:pose_tracker': typeof PoseTrackerClient;
|
|
16
|
+
readonly 'rdk:component:power_sensor': typeof PowerSensorClient;
|
|
17
|
+
readonly 'rdk:component:sensor': typeof SensorClient;
|
|
18
|
+
readonly 'rdk:component:servo': typeof ServoClient;
|
|
19
|
+
readonly 'rdk:component:switch': typeof SwitchClient;
|
|
20
|
+
readonly 'rdk:service:data_manager': typeof DataManagerClient;
|
|
21
|
+
readonly 'rdk:service:discovery': typeof DiscoveryClient;
|
|
22
|
+
readonly 'rdk:service:generic': typeof GenericServiceClient;
|
|
23
|
+
readonly 'rdk:service:mlmodel': typeof MLModelClient;
|
|
24
|
+
readonly 'rdk:service:motion': typeof MotionClient;
|
|
25
|
+
readonly 'rdk:service:navigation': typeof NavigationClient;
|
|
26
|
+
readonly 'rdk:service:slam': typeof SlamClient;
|
|
27
|
+
readonly 'rdk:service:vision': typeof VisionClient;
|
|
28
|
+
readonly 'rdk:service:world_state_store': typeof WorldStateStoreClient;
|
|
29
|
+
readonly 'rdk:service:video': typeof VideoClient;
|
|
30
|
+
};
|
|
31
|
+
export declare const clientForBuiltinResource: (resource: ResourceName) => typeof ArmClient | typeof BaseClient | typeof GantryClient | typeof GripperClient | typeof MotorClient | typeof ServoClient | typeof BoardClient | typeof ButtonClient | typeof CameraClient | typeof DiscoveryClient | typeof EncoderClient | typeof GenericComponentClient | typeof InputControllerClient | typeof MovementSensorClient | typeof PoseTrackerClient | typeof PowerSensorClient | typeof SensorClient | typeof SwitchClient | typeof DataManagerClient | typeof GenericServiceClient | typeof MLModelClient | typeof MotionClient | typeof NavigationClient | typeof SlamClient | typeof VisionClient | typeof WorldStateStoreClient | typeof VideoClient | undefined;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ArmClient, BaseClient, BoardClient, ButtonClient, CameraClient, DataManagerClient, DiscoveryClient, EncoderClient, GantryClient, GenericComponentClient, GenericServiceClient, GripperClient, InputControllerClient, MLModelClient, MotionClient, MotorClient, MovementSensorClient, NavigationClient, PoseTrackerClient, PowerSensorClient, SensorClient, ServoClient, SlamClient, SwitchClient, VideoClient, VisionClient, WorldStateStoreClient, } from '@viamrobotics/sdk';
|
|
2
|
+
import { getResourceAPI } from "./resource.js";
|
|
3
|
+
export const clientMap = {
|
|
4
|
+
'rdk:component:arm': ArmClient,
|
|
5
|
+
'rdk:component:base': BaseClient,
|
|
6
|
+
'rdk:component:board': BoardClient,
|
|
7
|
+
'rdk:component:button': ButtonClient,
|
|
8
|
+
'rdk:component:camera': CameraClient,
|
|
9
|
+
'rdk:component:encoder': EncoderClient,
|
|
10
|
+
'rdk:component:gantry': GantryClient,
|
|
11
|
+
'rdk:component:generic': GenericComponentClient,
|
|
12
|
+
'rdk:component:gripper': GripperClient,
|
|
13
|
+
'rdk:component:input_controller': InputControllerClient,
|
|
14
|
+
'rdk:component:motor': MotorClient,
|
|
15
|
+
'rdk:component:movement_sensor': MovementSensorClient,
|
|
16
|
+
'rdk:component:pose_tracker': PoseTrackerClient,
|
|
17
|
+
'rdk:component:power_sensor': PowerSensorClient,
|
|
18
|
+
'rdk:component:sensor': SensorClient,
|
|
19
|
+
'rdk:component:servo': ServoClient,
|
|
20
|
+
'rdk:component:switch': SwitchClient,
|
|
21
|
+
'rdk:service:data_manager': DataManagerClient,
|
|
22
|
+
'rdk:service:discovery': DiscoveryClient,
|
|
23
|
+
'rdk:service:generic': GenericServiceClient,
|
|
24
|
+
'rdk:service:mlmodel': MLModelClient,
|
|
25
|
+
'rdk:service:motion': MotionClient,
|
|
26
|
+
'rdk:service:navigation': NavigationClient,
|
|
27
|
+
'rdk:service:slam': SlamClient,
|
|
28
|
+
'rdk:service:vision': VisionClient,
|
|
29
|
+
'rdk:service:world_state_store': WorldStateStoreClient,
|
|
30
|
+
'rdk:service:video': VideoClient,
|
|
31
|
+
};
|
|
32
|
+
export const clientForBuiltinResource = (resource) => {
|
|
33
|
+
const resAPI = getResourceAPI(resource);
|
|
34
|
+
return resAPI in clientMap ? clientMap[resAPI] : undefined;
|
|
35
|
+
};
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
() => resourceName,
|
|
30
30
|
'camera'
|
|
31
31
|
)
|
|
32
|
-
const isShowingPip = $state(false)
|
|
33
32
|
let isShowingPointcloud = $state(false)
|
|
34
33
|
|
|
35
34
|
const { addImageToDataset } = useAddImageToDataset()
|
|
@@ -46,7 +45,6 @@
|
|
|
46
45
|
const imageQuery = createResourceQuery(client, 'getImages', () => ({
|
|
47
46
|
enabled: refetchInterval.current !== RefetchIntervals.LIVE,
|
|
48
47
|
refetchInterval: refetchInterval.current,
|
|
49
|
-
refetchIntervalInBackground: isShowingPip,
|
|
50
48
|
}))
|
|
51
49
|
|
|
52
50
|
const pointcloudQuery = createResourceQuery(client, 'getPointCloud', () => ({
|
|
@@ -70,11 +68,6 @@
|
|
|
70
68
|
// to be requested separately. Don't show a "toggle pip" button in Firefox.
|
|
71
69
|
const isFirefox = navigator.userAgent.toLowerCase().includes('firefox')
|
|
72
70
|
|
|
73
|
-
let mediaStream = $state<MediaStream | null>(null)
|
|
74
|
-
const setMediaStream = (next: MediaStream | null) => {
|
|
75
|
-
mediaStream = next
|
|
76
|
-
}
|
|
77
|
-
|
|
78
71
|
let mousePostionTooltip = $state<'On' | 'Off'>('Off')
|
|
79
72
|
const setMousePostionTooltip = (event: CustomEvent<string>) => {
|
|
80
73
|
mousePostionTooltip = event.detail as 'On' | 'Off'
|
|
@@ -103,7 +96,6 @@
|
|
|
103
96
|
error={imageQuery.error}
|
|
104
97
|
isLoading={imageQuery.isLoading}
|
|
105
98
|
refetch={imageQuery.refetch}
|
|
106
|
-
{setMediaStream}
|
|
107
99
|
showMousePositionTooltip={mousePostionTooltip === 'On'}
|
|
108
100
|
/>
|
|
109
101
|
</div>
|
|
@@ -146,9 +138,8 @@
|
|
|
146
138
|
{/if}
|
|
147
139
|
{#if !isFirefox}
|
|
148
140
|
<PictureInPictureButton
|
|
149
|
-
{mediaStream}
|
|
150
141
|
{resourceName}
|
|
151
|
-
{
|
|
142
|
+
rate={refetchInterval.current}
|
|
152
143
|
/>
|
|
153
144
|
{/if}
|
|
154
145
|
<Label>
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
isLoading: boolean
|
|
26
26
|
videoClass?: string
|
|
27
27
|
showMousePositionTooltip?: boolean
|
|
28
|
-
setMediaStream?: ((mediaStream: MediaStream | null) => void) | undefined
|
|
29
28
|
refetch: () => Promise<unknown>
|
|
30
29
|
}
|
|
31
30
|
|
|
@@ -39,7 +38,6 @@
|
|
|
39
38
|
isLoading,
|
|
40
39
|
videoClass = '',
|
|
41
40
|
showMousePositionTooltip = false,
|
|
42
|
-
setMediaStream: setPipMediaStream,
|
|
43
41
|
refetch,
|
|
44
42
|
}: Props = $props()
|
|
45
43
|
|
|
@@ -72,7 +70,6 @@
|
|
|
72
70
|
}
|
|
73
71
|
videoElement.srcObject = mediaStream
|
|
74
72
|
}
|
|
75
|
-
setPipMediaStream?.(mediaStream)
|
|
76
73
|
}
|
|
77
74
|
|
|
78
75
|
// For live streams, set the media stream when there is a new video track.
|
|
@@ -185,13 +182,12 @@
|
|
|
185
182
|
})
|
|
186
183
|
|
|
187
184
|
$effect(() => {
|
|
188
|
-
return () =>
|
|
185
|
+
return () =>
|
|
189
186
|
untrack(() => {
|
|
190
187
|
disableStream().then(() => {
|
|
191
188
|
setMediaStream(null)
|
|
192
189
|
})
|
|
193
190
|
})
|
|
194
|
-
}
|
|
195
191
|
})
|
|
196
192
|
|
|
197
193
|
$effect(() => {
|
|
@@ -10,7 +10,6 @@ interface Props {
|
|
|
10
10
|
isLoading: boolean;
|
|
11
11
|
videoClass?: string;
|
|
12
12
|
showMousePositionTooltip?: boolean;
|
|
13
|
-
setMediaStream?: ((mediaStream: MediaStream | null) => void) | undefined;
|
|
14
13
|
refetch: () => Promise<unknown>;
|
|
15
14
|
}
|
|
16
15
|
declare const LiveOrPollingVideo: import("svelte").Component<Props, {}, "">;
|
|
@@ -1,80 +1,35 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Button } from '@viamrobotics/prime-core'
|
|
3
|
-
import { onMount } from 'svelte'
|
|
4
3
|
|
|
5
4
|
import ErrorDisplay from '../../error.svelte'
|
|
6
5
|
|
|
6
|
+
import { usePip } from '../../../pip/context.svelte'
|
|
7
|
+
|
|
7
8
|
interface Props {
|
|
8
9
|
resourceName: string
|
|
9
|
-
|
|
10
|
-
isShowingPip: boolean
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
let { resourceName, mediaStream, isShowingPip = $bindable(false) }: Props = $props()
|
|
14
|
-
|
|
15
|
-
let videoElement = $state.raw<HTMLVideoElement>()
|
|
16
|
-
let lastErr = $state.raw<Error>()
|
|
17
|
-
|
|
18
|
-
const onEnter = () => {
|
|
19
|
-
isShowingPip = true
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const onLeave = () => {
|
|
23
|
-
isShowingPip = false
|
|
10
|
+
rate: number | 'live' | false
|
|
24
11
|
}
|
|
25
12
|
|
|
26
|
-
|
|
27
|
-
videoElement?.addEventListener('enterpictureinpicture', onEnter)
|
|
28
|
-
videoElement?.addEventListener('leavepictureinpicture', onLeave)
|
|
13
|
+
const { resourceName, rate }: Props = $props()
|
|
29
14
|
|
|
30
|
-
|
|
31
|
-
videoElement?.removeEventListener('enterpictureinpicture', onEnter)
|
|
32
|
-
videoElement?.removeEventListener('leavepictureinpicture', onLeave)
|
|
33
|
-
if (isShowingPip) {
|
|
34
|
-
document.exitPictureInPicture()
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
})
|
|
15
|
+
const pip = usePip()
|
|
38
16
|
|
|
39
|
-
$effect
|
|
40
|
-
if (
|
|
41
|
-
|
|
17
|
+
$effect(() => {
|
|
18
|
+
if (resourceName === pip.resourceName) {
|
|
19
|
+
pip.setRate(rate)
|
|
42
20
|
}
|
|
43
21
|
})
|
|
44
|
-
|
|
45
|
-
const togglePictureInPicture = async () => {
|
|
46
|
-
lastErr = undefined
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
await (isShowingPip
|
|
50
|
-
? document.exitPictureInPicture()
|
|
51
|
-
: videoElement?.requestPictureInPicture())
|
|
52
|
-
} catch (error) {
|
|
53
|
-
lastErr = error as Error
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
22
|
</script>
|
|
57
23
|
|
|
58
24
|
<Button
|
|
59
25
|
icon="picture-in-picture-top-right"
|
|
60
|
-
onclick={
|
|
26
|
+
onclick={() => pip.toggle(resourceName)}
|
|
27
|
+
progress={pip.readyState === 'loading' ? 'indeterminate' : undefined}
|
|
61
28
|
>
|
|
62
29
|
Toggle picture-in-picture
|
|
63
30
|
</Button>
|
|
64
31
|
|
|
65
|
-
<!-- Display a fixed video element on the page but essentially hide it.
|
|
66
|
-
There must always be a video element visible for PiP. -->
|
|
67
|
-
<video
|
|
68
|
-
class="fixed right-0 bottom-0 h-px w-px opacity-[0.01]"
|
|
69
|
-
bind:this={videoElement}
|
|
70
|
-
muted
|
|
71
|
-
autoplay
|
|
72
|
-
controls={false}
|
|
73
|
-
playsinline
|
|
74
|
-
aria-label={`${resourceName} picture-in-picture stream`}
|
|
75
|
-
></video>
|
|
76
|
-
|
|
77
32
|
<ErrorDisplay
|
|
78
33
|
class="max-w-50"
|
|
79
|
-
lastError={
|
|
34
|
+
lastError={pip.error}
|
|
80
35
|
/>
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
interface Props {
|
|
2
2
|
resourceName: string;
|
|
3
|
-
|
|
4
|
-
isShowingPip: boolean;
|
|
3
|
+
rate: number | 'live' | false;
|
|
5
4
|
}
|
|
6
|
-
declare const PictureInPictureButton: import("svelte").Component<Props, {}, "
|
|
5
|
+
declare const PictureInPictureButton: import("svelte").Component<Props, {}, "">;
|
|
7
6
|
type PictureInPictureButton = ReturnType<typeof PictureInPictureButton>;
|
|
8
7
|
export default PictureInPictureButton;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { createResourceClient, createResourceMutation } from '@viamrobotics/svelte-sdk'
|
|
6
6
|
import { PersistedState } from 'runed'
|
|
7
7
|
|
|
8
|
-
import { clientForBuiltinResource } from '../../../
|
|
8
|
+
import { clientForBuiltinResource } from '../../../client-map'
|
|
9
9
|
import { getResourceAPI, getResourceKey } from '../../../resource'
|
|
10
10
|
|
|
11
11
|
import ErrorDisplay from '../../error.svelte'
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface PipContext {
|
|
2
|
+
/** Called by PictureInPictureButton to start or stop PiP. */
|
|
3
|
+
toggle(resourceName: string): Promise<void>;
|
|
4
|
+
setRate(rate: 'live' | number | false): void;
|
|
5
|
+
error: Error | undefined;
|
|
6
|
+
/** Whether the PictureInPicture is active */
|
|
7
|
+
readyState: 'inactive' | 'loading' | 'active';
|
|
8
|
+
/** The current resource streamed */
|
|
9
|
+
resourceName: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
export declare function providePip(partID: () => string): PipContext;
|
|
12
|
+
export declare function usePip(): PipContext;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { CameraClient } from '@viamrobotics/sdk';
|
|
2
|
+
import { createResourceClient, createResourceQuery, createStreamClient, } from '@viamrobotics/svelte-sdk';
|
|
3
|
+
import { getContext, setContext } from 'svelte';
|
|
4
|
+
const PIP_KEY = Symbol('pip-context');
|
|
5
|
+
export function providePip(partID) {
|
|
6
|
+
const video = document.createElement('video');
|
|
7
|
+
video.muted = true;
|
|
8
|
+
video.autoplay = true;
|
|
9
|
+
video.controls = false;
|
|
10
|
+
video.playsInline = true;
|
|
11
|
+
video.ariaHidden = 'true';
|
|
12
|
+
video.style = 'position:fixed; right: 0; bottom: 0; height: 1px; width: 1px; opacity: 0.01;';
|
|
13
|
+
const canvas = document.createElement('canvas');
|
|
14
|
+
const canvasCtx = canvas.getContext('2d');
|
|
15
|
+
const canvasStream = canvas.captureStream();
|
|
16
|
+
const img = document.createElement('img');
|
|
17
|
+
let error = $state();
|
|
18
|
+
let readyState = $state('inactive');
|
|
19
|
+
let rate = $state('live');
|
|
20
|
+
let activeName = $state();
|
|
21
|
+
const cameraClient = $derived(activeName ? createResourceClient(CameraClient, partID, () => activeName ?? '') : undefined);
|
|
22
|
+
const streamClient = $derived(activeName ? createStreamClient(partID, () => activeName ?? '') : undefined);
|
|
23
|
+
const imageQuery = $derived(cameraClient?.current
|
|
24
|
+
? createResourceQuery(cameraClient, 'getImages', () => ({
|
|
25
|
+
enabled: rate !== 'live',
|
|
26
|
+
refetchInterval: typeof rate === 'number' ? rate : false,
|
|
27
|
+
refetchIntervalInBackground: activeName === cameraClient?.current?.name,
|
|
28
|
+
}))
|
|
29
|
+
: undefined);
|
|
30
|
+
const mediaStream = $derived((rate === 'live' ? streamClient?.mediaStream : canvasStream) ?? null);
|
|
31
|
+
$effect(() => {
|
|
32
|
+
document.body.append(video);
|
|
33
|
+
return () => {
|
|
34
|
+
video.remove();
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
$effect(() => {
|
|
38
|
+
video.srcObject = mediaStream;
|
|
39
|
+
return () => {
|
|
40
|
+
video.srcObject = null;
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
$effect(() => {
|
|
44
|
+
const currentRate = rate;
|
|
45
|
+
if (currentRate === 'live')
|
|
46
|
+
return;
|
|
47
|
+
const image = imageQuery?.data?.images[0];
|
|
48
|
+
if (!image)
|
|
49
|
+
return;
|
|
50
|
+
if (img.src)
|
|
51
|
+
URL.revokeObjectURL(img.src);
|
|
52
|
+
img.src = URL.createObjectURL(new Blob([new Uint8Array(image.image)], {
|
|
53
|
+
type: image.mimeType || 'image/jpeg',
|
|
54
|
+
}));
|
|
55
|
+
const drawImage = () => {
|
|
56
|
+
canvas.width = img.naturalWidth;
|
|
57
|
+
canvas.height = img.naturalHeight;
|
|
58
|
+
canvasCtx?.drawImage(img, 0, 0);
|
|
59
|
+
};
|
|
60
|
+
img.addEventListener('load', drawImage);
|
|
61
|
+
return () => {
|
|
62
|
+
if (img.src)
|
|
63
|
+
URL.revokeObjectURL(img.src);
|
|
64
|
+
img.removeEventListener('load', drawImage);
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
const toggle = async () => {
|
|
68
|
+
if (activeName === undefined &&
|
|
69
|
+
readyState === 'active' &&
|
|
70
|
+
document.pictureInPictureElement === video) {
|
|
71
|
+
try {
|
|
72
|
+
await document.exitPictureInPicture();
|
|
73
|
+
readyState = 'inactive';
|
|
74
|
+
error = undefined;
|
|
75
|
+
}
|
|
76
|
+
catch (nextError) {
|
|
77
|
+
error = nextError;
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
readyState = 'loading';
|
|
82
|
+
// Wait for the new stream's metadata to load before requesting PiP,
|
|
83
|
+
// otherwise the browser throws InvalidStateError.
|
|
84
|
+
if (video.readyState < 1 /* HAVE_METADATA */) {
|
|
85
|
+
await new Promise((resolve) => {
|
|
86
|
+
video.addEventListener('loadedmetadata', () => resolve(), { once: true });
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await video.requestPictureInPicture();
|
|
91
|
+
readyState = 'active';
|
|
92
|
+
error = undefined;
|
|
93
|
+
}
|
|
94
|
+
catch (nextError) {
|
|
95
|
+
error = nextError;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const context = {
|
|
99
|
+
async toggle(resourceName) {
|
|
100
|
+
activeName = activeName === resourceName ? undefined : resourceName;
|
|
101
|
+
await toggle();
|
|
102
|
+
},
|
|
103
|
+
setRate(newRate) {
|
|
104
|
+
rate = newRate;
|
|
105
|
+
},
|
|
106
|
+
get error() {
|
|
107
|
+
return error;
|
|
108
|
+
},
|
|
109
|
+
get readyState() {
|
|
110
|
+
return readyState;
|
|
111
|
+
},
|
|
112
|
+
get resourceName() {
|
|
113
|
+
return activeName;
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
setContext(PIP_KEY, context);
|
|
117
|
+
return context;
|
|
118
|
+
}
|
|
119
|
+
export function usePip() {
|
|
120
|
+
const ctx = getContext(PIP_KEY);
|
|
121
|
+
if (!ctx) {
|
|
122
|
+
throw new Error('usePipContext must be called after providePip()');
|
|
123
|
+
}
|
|
124
|
+
return ctx;
|
|
125
|
+
}
|