@lincyaw/addon-lecture 1.0.0-rc.1 → 1.0.0-rc.10
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 +9 -9
- package/src/client/PlayerPanel.css +33 -0
- package/src/client/PlayerPanel.tsx +6 -2
- package/src/client/clock.ts +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lincyaw/addon-lecture",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.10",
|
|
4
4
|
"description": "Presmium plugin: lecture tracks: recording, playback, scripted lectures with speech, and video export (presmium-lecture)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"playwright-chromium": "^1.40.0",
|
|
42
42
|
"react": "^18.3.1",
|
|
43
43
|
"react-dom": "^18.3.1",
|
|
44
|
-
"@lincyaw/
|
|
45
|
-
"@lincyaw/
|
|
46
|
-
"@lincyaw/
|
|
44
|
+
"@lincyaw/compiler": "^1.0.0-rc.10",
|
|
45
|
+
"@lincyaw/host": "^1.0.0-rc.10",
|
|
46
|
+
"@lincyaw/kernel": "^1.0.0-rc.10"
|
|
47
47
|
},
|
|
48
48
|
"peerDependenciesMeta": {
|
|
49
49
|
"playwright-chromium": {
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
"@types/react": "18.3.28",
|
|
58
58
|
"react-dom": "18.3.1",
|
|
59
59
|
"@types/react-dom": "18.3.7",
|
|
60
|
-
"@lincyaw/cli": "1.0.0-rc.
|
|
61
|
-
"@lincyaw/
|
|
62
|
-
"@lincyaw/
|
|
63
|
-
"@lincyaw/viewer": "1.0.0-rc.
|
|
64
|
-
"@lincyaw/
|
|
60
|
+
"@lincyaw/cli": "1.0.0-rc.10",
|
|
61
|
+
"@lincyaw/host": "1.0.0-rc.10",
|
|
62
|
+
"@lincyaw/compiler": "1.0.0-rc.10",
|
|
63
|
+
"@lincyaw/viewer": "1.0.0-rc.10",
|
|
64
|
+
"@lincyaw/kernel": "1.0.0-rc.10"
|
|
65
65
|
},
|
|
66
66
|
"presmium": {
|
|
67
67
|
"preview": {
|
|
@@ -118,3 +118,36 @@ html.pm-lecture-exporting .pm-slide-fit__canvas {
|
|
|
118
118
|
display: none;
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
+
|
|
122
|
+
/* A lecture the browser would not autoplay waits here, centred on the slide, for the reader's click. */
|
|
123
|
+
.pm-lecture-start {
|
|
124
|
+
position: fixed;
|
|
125
|
+
inset: 0;
|
|
126
|
+
z-index: var(--pm-z-lecture-player);
|
|
127
|
+
display: grid;
|
|
128
|
+
place-items: center;
|
|
129
|
+
pointer-events: none;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.pm-lecture-start__button {
|
|
133
|
+
display: grid;
|
|
134
|
+
place-items: center;
|
|
135
|
+
width: var(--pm-size-start);
|
|
136
|
+
height: var(--pm-size-start);
|
|
137
|
+
border: 0;
|
|
138
|
+
border-radius: var(--pm-radius-pill);
|
|
139
|
+
background: color-mix(in srgb, var(--pm-color-ink) 78%, transparent);
|
|
140
|
+
color: var(--pm-color-bg);
|
|
141
|
+
box-shadow: var(--pm-shadow-float);
|
|
142
|
+
cursor: pointer;
|
|
143
|
+
pointer-events: auto;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.pm-lecture-start__button:hover {
|
|
147
|
+
background: var(--pm-color-ink);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.pm-lecture-start__button .pm-ui-icon {
|
|
151
|
+
width: var(--pm-size-start-icon);
|
|
152
|
+
height: var(--pm-size-start-icon);
|
|
153
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PmCaption, PmDrawer, PmIconButton, PmMenu, PmTabs, PRODUCTION_PLAYBACK, TIMED_CAPTIONS, useMessages, useServer, useService, useStore } from '@lincyaw/host'
|
|
1
|
+
import { PmCaption, PmDrawer, PmIcon, PmIconButton, PmMenu, PmTabs, PRODUCTION_PLAYBACK, TIMED_CAPTIONS, useMessages, useServer, useService, useStore } from '@lincyaw/host'
|
|
2
2
|
import type { MenuItem } from '@lincyaw/host'
|
|
3
3
|
import { isPreviewEmbedded } from '@lincyaw/kernel'
|
|
4
4
|
import { useEffect, useMemo, useState } from 'react'
|
|
@@ -47,7 +47,8 @@ export default function PlayerPanel(_props: { view?: string }) {
|
|
|
47
47
|
const timedCaptions = useService(TIMED_CAPTIONS)
|
|
48
48
|
const production = useService(PRODUCTION_PLAYBACK)
|
|
49
49
|
const controlled = useStore(production instanceof ProductionPlaybackController ? production.engaged : undefined, false)
|
|
50
|
-
|
|
50
|
+
// The lecture times the captions from the moment it is asked for: while it loads, the slides show none of their own.
|
|
51
|
+
useEffect(() => id && !failure && status !== 'missing' ? timedCaptions?.claim() : undefined, [id, failure, status, timedCaptions])
|
|
51
52
|
useEffect(() => {
|
|
52
53
|
if (!player || status !== 'ready' || failure || controlled || exporting) return
|
|
53
54
|
keyboardPlayer.set(player)
|
|
@@ -57,6 +58,8 @@ export default function PlayerPanel(_props: { view?: string }) {
|
|
|
57
58
|
}
|
|
58
59
|
return () => { if (keyboardPlayer.getSnapshot() === player) keyboardPlayer.set(undefined) }
|
|
59
60
|
}, [player, status, failure, controlled])
|
|
61
|
+
// The browser would not start the sound without the reader: a large Play waits for their click.
|
|
62
|
+
const blocked = useStore(player?.clock.blocked, false)
|
|
60
63
|
const time = useStore(player?.time, 0),
|
|
61
64
|
clockPlaying = useStore(player?.clock.playing, false),
|
|
62
65
|
rate = useStore(player?.clock.rate, 1)
|
|
@@ -172,6 +175,7 @@ export default function PlayerPanel(_props: { view?: string }) {
|
|
|
172
175
|
else if (option === 'close') play(undefined)
|
|
173
176
|
}
|
|
174
177
|
return <>
|
|
178
|
+
{blocked && <div className="pm-lecture-start"><button type="button" className="pm-lecture-start__button" aria-label={t.player.play} onClick={() => void player.play()}><PmIcon name="play" /></button></div>}
|
|
175
179
|
{(captions || current?.unsaid) && (current || manual) && caption && <PmCaption overlay label={t.player.captions} lines={[caption]} lang={lang}
|
|
176
180
|
data-stale={current && stale.has(current.id) ? 'true' : undefined} hint={current && stale.has(current.id) ? t.player.stale : undefined} />}
|
|
177
181
|
{panel && <PmDrawer open label={panel === 'transcript' ? t.player.transcript : t.player.chapters} onClose={() => setPanel(undefined)}>
|
package/src/client/clock.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface TrackClock {
|
|
|
6
6
|
readonly playing: Readonly<Store<boolean>>
|
|
7
7
|
readonly rate: Readonly<Store<number>>
|
|
8
8
|
readonly error: Readonly<Store<Error | undefined>>
|
|
9
|
+
/** The browser refused to start the sound before the reader acted on this page (autoplay): playback waits for them. */
|
|
10
|
+
readonly blocked: Readonly<Store<boolean>>
|
|
9
11
|
play(): void
|
|
10
12
|
pause(): void
|
|
11
13
|
seek(t: number): void
|
|
@@ -18,6 +20,7 @@ export class SilentClock implements TrackClock {
|
|
|
18
20
|
readonly playing = createStore(false)
|
|
19
21
|
readonly rate = createStore(1)
|
|
20
22
|
readonly error = createStore<Error | undefined>(undefined)
|
|
23
|
+
readonly blocked = createStore(false)
|
|
21
24
|
protected at = 0
|
|
22
25
|
protected since = performance.now()
|
|
23
26
|
protected readonly duration: number
|
|
@@ -33,6 +36,7 @@ export class SilentClock implements TrackClock {
|
|
|
33
36
|
|
|
34
37
|
play(): void {
|
|
35
38
|
if (this.playing.getSnapshot()) return
|
|
39
|
+
this.blocked.set(false)
|
|
36
40
|
if (this.at >= this.duration) this.at = 0
|
|
37
41
|
this.since = performance.now()
|
|
38
42
|
this.playing.set(true)
|
|
@@ -150,6 +154,12 @@ export class MediaClock extends SilentClock {
|
|
|
150
154
|
if (!this.playing.getSnapshot() || at < m.start || at >= m.start + m.duration) m.el.pause()
|
|
151
155
|
}).catch((error: unknown) => {
|
|
152
156
|
if (error instanceof DOMException && error.name === 'AbortError') return
|
|
157
|
+
// Autoplay without the reader's gesture: not a failure, playback waits for their click.
|
|
158
|
+
if (error instanceof DOMException && error.name === 'NotAllowedError') {
|
|
159
|
+
this.pause()
|
|
160
|
+
this.blocked.set(true)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
153
163
|
this.fail(error instanceof Error ? error : new Error(String(error)))
|
|
154
164
|
})
|
|
155
165
|
}
|