Skip to content

Commit 2fccd65

Browse files
Rich-HarrisRich Harris
andauthored
Media elements (sveltejs#336)
* media elements * better media elements example * update module-context exercise * fix * update module-exports exercise --------- Co-authored-by: Rich Harris <[email protected]>
1 parent a0314a3 commit 2fccd65

File tree

25 files changed

+1012
-628
lines changed

25 files changed

+1012
-628
lines changed

content/tutorial/02-advanced-svelte/05-bindings/03-media-elements/README.md

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,66 @@
22
title: Media elements
33
---
44

5-
> This exercise doesn't currently work. You can switch to the old tutorial instead: https://svelte.dev/tutorial/media-elements
5+
You can bind to properties of `<audio>` and `<video>` elements, making it easy to (for example) build custom player UI, like `AudioPlayer.svelte`.
66

7-
The `<audio>` and `<video>` elements have several properties that you can bind to. This example demonstrates a few of them.
7+
First, add the `<audio>` element along with its bindings (we'll use the shorthand form for `duration` and `paused`):
88

9-
On line 62, add `currentTime={time}`, `duration` and `paused` bindings:
9+
```svelte
10+
/// file: AudioPlayer.svelte
11+
<div class="player" class:paused>
12+
+++ <audio
13+
src={src}
14+
bind:currentTime={time}
15+
bind:duration
16+
bind:paused
17+
/>+++
18+
19+
<button
20+
class="play"
21+
aria-label={paused ? 'play' : 'pause'}
22+
/>
23+
```
24+
25+
Next, add an event handler to the `<button>` that toggles `paused`:
1026

1127
```svelte
12-
/// file: App.svelte
13-
<video
14-
poster="https://sveltejs.github.io/assets/caminandes-llamigos.jpg"
15-
src="https://sveltejs.github.io/assets/caminandes-llamigos.mp4"
16-
on:mousemove={handleMove}
17-
on:touchmove|preventDefault={handleMove}
18-
on:mousedown={handleMousedown}
19-
on:mouseup={handleMouseup}
20-
bind:currentTime={time}
21-
bind:duration
22-
bind:paused>
23-
<track kind="captions">
24-
</video>
28+
/// file: AudioPlayer.svelte
29+
<button
30+
class="play"
31+
aria-label={paused ? 'play' : 'pause'}
32+
+++on:click={() => paused = !paused}+++
33+
/>
2534
```
2635

27-
> `bind:duration` is equivalent to `bind:duration={duration}`
36+
Our audio player now has basic functionality. Let's add the ability to seek to a specific part of a track by dragging the slider. Inside the slider's `pointerdown` handler there's a `seek` function, where we can update `time`:
37+
38+
```js
39+
/// file: AudioPlayer.svelte
40+
function seek(e) {
41+
const { left, width } = div.getBoundingClientRect();
2842

29-
Now, when you click on the video, it will update `time`, `duration` and `paused` as appropriate. This means we can use them to build custom controls.
43+
let p = (e.clientX - left) / width;
44+
if (p < 0) p = 0;
45+
if (p > 1) p = 1;
3046

31-
> Ordinarily on the web, you would track `currentTime` by listening for `timeupdate` events. But these events fire too infrequently, resulting in choppy UI. Svelte does better — it checks `currentTime` using `requestAnimationFrame`.
47+
+++time = p * duration;+++
48+
}
49+
```
50+
51+
When the track ends, be kind — rewind:
52+
53+
```svelte
54+
/// file: AudioPlayer.svelte
55+
<audio
56+
src={src}
57+
bind:currentTime={time}
58+
bind:duration
59+
bind:paused
60+
+++ on:ended={() => {
61+
time = 0;
62+
}}+++
63+
/>
64+
```
3265

3366
The complete set of bindings for `<audio>` and `<video>` is as follows — six _readonly_ bindings...
3467

Lines changed: 15 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -1,158 +1,22 @@
11
<script>
2-
// These values are bound to properties of the video
3-
let time = 0;
4-
let duration;
5-
let paused = true;
6-
7-
let showControls = true;
8-
let showControlsTimeout;
9-
10-
// Used to track time of last mouse down event
11-
let lastMouseDown;
12-
13-
function handleMove(e) {
14-
// Make the controls visible, but fade out after
15-
// 2.5 seconds of inactivity
16-
clearTimeout(showControlsTimeout);
17-
showControlsTimeout = setTimeout(
18-
() => (showControls = false),
19-
2500
20-
);
21-
showControls = true;
22-
23-
if (!duration) return; // video not loaded yet
24-
if (
25-
e.type !== 'touchmove' &&
26-
!(e.buttons & 1)
27-
)
28-
return; // mouse not down
29-
30-
const clientX =
31-
e.type === 'touchmove'
32-
? e.touches[0].clientX
33-
: e.clientX;
34-
const { left, right } =
35-
this.getBoundingClientRect();
36-
time =
37-
(duration * (clientX - left)) /
38-
(right - left);
39-
}
40-
41-
// we can't rely on the built-in click event, because it fires
42-
// after a drag — we have to listen for clicks ourselves
43-
function handleMousedown(e) {
44-
lastMouseDown = new Date();
45-
}
46-
47-
function handleMouseup(e) {
48-
if (new Date() - lastMouseDown < 300) {
49-
if (paused) e.target.play();
50-
else e.target.pause();
51-
}
52-
}
53-
54-
function format(seconds) {
55-
if (isNaN(seconds)) return '...';
56-
57-
const minutes = Math.floor(seconds / 60);
58-
seconds = Math.floor(seconds % 60);
59-
if (seconds < 10) seconds = '0' + seconds;
60-
61-
return `${minutes}:${seconds}`;
62-
}
2+
import AudioPlayer from './AudioPlayer.svelte';
3+
import { tracks } from './tracks.js';
634
</script>
645

65-
<h1>Caminandes: Llamigos</h1>
66-
<p>
67-
From <a
68-
href="https://cloud.blender.org/open-projects"
69-
>Blender Open Projects</a
70-
>. CC-BY
71-
</p>
72-
73-
<div>
74-
<video
75-
poster="https://sveltejs.github.io/assets/caminandes-llamigos.jpg"
76-
src="https://sveltejs.github.io/assets/caminandes-llamigos.mp4"
77-
on:mousemove={handleMove}
78-
on:touchmove|preventDefault={handleMove}
79-
on:mousedown={handleMousedown}
80-
on:mouseup={handleMouseup}
81-
>
82-
<track kind="captions" />
83-
</video>
84-
85-
<div
86-
class="controls"
87-
style="opacity: {duration && showControls
88-
? 1
89-
: 0}"
90-
>
91-
<progress value={time / duration || 0} />
92-
93-
<div class="info">
94-
<span class="time">{format(time)}</span>
95-
<span
96-
>click anywhere to {paused
97-
? 'play'
98-
: 'pause'} / drag to seek</span
99-
>
100-
<span class="time">{format(duration)}</span>
101-
</div>
102-
</div>
6+
<div class="centered">
7+
{#each tracks as track}
8+
<AudioPlayer {...track} />
9+
{/each}
10310
</div>
10411

10512
<style>
106-
div {
107-
position: relative;
108-
}
109-
110-
.controls {
111-
position: absolute;
112-
top: 0;
113-
width: 100%;
114-
transition: opacity 1s;
115-
}
116-
117-
.info {
13+
.centered {
11814
display: flex;
119-
width: 100%;
120-
justify-content: space-between;
121-
}
122-
123-
span {
124-
padding: 0.2em 0.5em;
125-
color: white;
126-
text-shadow: 0 0 8px black;
127-
font-size: 1.4em;
128-
opacity: 0.7;
129-
}
130-
131-
.time {
132-
width: 3em;
133-
}
134-
135-
.time:last-child {
136-
text-align: right;
137-
}
138-
139-
progress {
140-
display: block;
141-
width: 100%;
142-
height: 10px;
143-
-webkit-appearance: none;
144-
appearance: none;
145-
}
146-
147-
progress::-webkit-progress-bar {
148-
background-color: rgba(0, 0, 0, 0.2);
149-
}
150-
151-
progress::-webkit-progress-value {
152-
background-color: rgba(255, 255, 255, 0.6);
153-
}
154-
155-
video {
156-
width: 100%;
157-
}
158-
</style>
15+
flex-direction: column;
16+
height: 100%;
17+
justify-content: center;
18+
gap: 0.5em;
19+
max-width: 40em;
20+
margin: 0 auto;
21+
}
22+
</style>
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<script>
2+
export let src;
3+
export let title;
4+
export let artist;
5+
6+
let time = 0;
7+
let duration = 0;
8+
let paused = true;
9+
10+
function format(time) {
11+
if (isNaN(time)) return '...';
12+
13+
const minutes = Math.floor(time / 60);
14+
const seconds = Math.floor(time % 60);
15+
16+
return `${minutes}:${seconds < 10 ? `0${seconds}` : seconds}`;
17+
}
18+
</script>
19+
20+
<div class="player" class:paused>
21+
<button
22+
class="play"
23+
aria-label={paused ? 'play' : 'pause'}
24+
/>
25+
26+
<div class="info">
27+
<div class="description">
28+
<strong>{title}</strong> /
29+
<span>{artist}</span>
30+
</div>
31+
32+
<div class="time">
33+
<span>{format(time)}</span>
34+
<div
35+
class="slider"
36+
on:pointerdown={e => {
37+
const div = e.currentTarget;
38+
39+
function seek(e) {
40+
const { left, width } = div.getBoundingClientRect();
41+
42+
let p = (e.clientX - left) / width;
43+
if (p < 0) p = 0;
44+
if (p > 1) p = 1;
45+
46+
// TODO update the time
47+
}
48+
49+
seek(e);
50+
51+
window.addEventListener('pointermove', seek);
52+
53+
window.addEventListener('pointerup', () => {
54+
window.removeEventListener('pointermove', seek);
55+
}, {
56+
once: true
57+
});
58+
}}
59+
>
60+
<div class="progress" style="--progress: {time / duration}%" />
61+
</div>
62+
<span>{duration ? format(duration) : '--:--'}</span>
63+
</div>
64+
</div>
65+
</div>
66+
67+
<style>
68+
.player {
69+
display: grid;
70+
grid-template-columns: 2.5em 1fr;
71+
align-items: center;
72+
gap: 1em;
73+
padding: 0.5em 1em 0.5em 0.5em;
74+
border-radius: 2em;
75+
background: var(--bg-1);
76+
transition: filter 0.2s;
77+
color: var(--fg-3);
78+
user-select: none;
79+
}
80+
81+
.player:not(.paused) {
82+
color: var(--fg-1);
83+
filter: drop-shadow(0.5em 0.5em 1em rgba(0,0,0,0.1));
84+
}
85+
86+
button {
87+
width: 100%;
88+
aspect-ratio: 1;
89+
background-repeat: no-repeat;
90+
background-position: 50% 50%;
91+
border-radius: 50%;
92+
}
93+
94+
[aria-label="pause"] {
95+
background-image: url(./pause.svg);
96+
}
97+
98+
[aria-label="play"] {
99+
background-image: url(./play.svg);
100+
}
101+
102+
.info {
103+
overflow: hidden;
104+
}
105+
106+
.description {
107+
white-space: nowrap;
108+
overflow: hidden;
109+
text-overflow: ellipsis;
110+
line-height: 1.2;
111+
}
112+
113+
.time {
114+
display: flex;
115+
align-items: center;
116+
gap: 0.5em;
117+
}
118+
119+
.time span {
120+
font-size: 0.7em;
121+
}
122+
123+
.slider {
124+
flex: 1;
125+
height: 0.5em;
126+
background: var(--bg-2);
127+
border-radius: 0.5em;
128+
overflow: hidden;
129+
}
130+
131+
.progress {
132+
width: calc(100 * var(--progress));
133+
height: 100%;
134+
background: var(--bg-3);
135+
}
136+
</style>

0 commit comments

Comments
 (0)