如何自製 Video Player
準備一個原生的
Video Player
如何自製 Video Player
如何自製 Video Player
客製化原本的
Video Player 樣式
具體需要做哪些事?
Event Handling
- Basic Events (Play/Pause/waiting...)
- Time & Progress
- Seeking
Basic Events
Basic Events
- Load
- Waiting
- Play
- Pause
- End
// video events
React.useEffect(() => {
const vid = videoRef.current;
const duration = durationRef.current;
if (duration)
vid.onloadeddata = () => {
duration.textContent = getTimeStr(vid.duration);
};
vid.onplay = () => {
setPlaying(true);
};
vid.onpause = () => {
setPlaying(false);
};
vid.onwaiting = () => {
setLoading(true);
};
vid.oncanplay = () => {
if (!vid.seeking) {
setLoading(false);
setEnded(false);
}
};
return () => {
vid.onplay = null;
vid.onpause = null;
vid.onwaiting = null;
vid.oncanplay = null;
};
}, [durationRef, readyToLoad, setEnded, setLoading, setPlaying, videoRef]);Play & Pause
var playPromise = document.querySelector('video').play();
// In browsers that don’t yet support this functionality,
// playPromise won’t be defined.
if (playPromise !== undefined) {
playPromise.then(function() {
// Automatic playback started!
}).catch(function(error) {
// Automatic playback failed.
// Show a UI element to let the user manually start playback.
});
}Time & Progress
- Current Time / Duration
- Loaded Progress
Current Time & Duration
Current Time & Duration
React.useEffect(() => {
const vid = videoRef.current;
if (!readyToLoad || !vid) return;
vid.ontimeupdate = () => {
if (!vid.seeking) handleTimeChange(vid.currentTime, vid.duration);
};
vid.onseeking = () => {
handleTimeChange(vid.currentTime, vid.duration);
};
return () => {
vid.ontimeupdate = null;
vid.onseeking = null;
};
}, [
handleTimeChange,
readyToLoad,
videoRef,
]);
Loaded Progress Bar
Progress Bar
const backgroundImage = `linear-gradient(90deg, rgb(51, 151, 207) 0%, rgb(51, 151, 207) 30%, transparent 30%, transparent 100%);`Loaded Progress Bar
let linearGradientBody = '';
for (let i = 0; i < vid.buffered.length; i++) {
// firefox will have error
const startPercent = trimTimePercent(
(vid.buffered.start(i) / duration) * 100
);
const endPercent = trimTimePercent(
(vid.buffered.end(i) / duration) * 100
);
if (endPercent > currentPercent)
linearGradientBody += `${basicColor} ${startPercent}%, ${bufferedColor} ${startPercent}%, ${bufferedColor} ${endPercent}%, ${basicColor} ${endPercent}%, `;
}Seeking
React.useEffect(() => {
let timer: number | undefined;
const handleMouseDown = () => {
prevPlayingStatusRef.current = playing;
// NOTE: PART1 prevent video didn't receive onplay event if click too fast
timer = window.setTimeout(() => vid.pause(), 50);
};
const handleMouseUp = () => {
if (prevPlayingStatusRef.current && vid.currentTime < vid.duration) {
// NOTE: PART2 prevent video didn't receive on play event if click too fast
if (typeof timer === 'number') window.clearTimeout(timer);
vid.play();
}
};
}, [readyToLoad, playing, videoRef, progressBarRef]);Fullscreen
Fullscreen
- Different Browsers' API
- The Behavior of Safari
Fullscreen
(function () {
'use strict';
const document =
typeof window !== 'undefined' && typeof window.document !== 'undefined'
? window.document
: {};
const isCommonjs = typeof module !== 'undefined' && module.exports;
const fn = (function () {
let val;
const fnMap = [
[
'requestFullscreen',
'exitFullscreen',
'fullscreenElement',
'fullscreenEnabled',
'fullscreenchange',
'fullscreenerror',
],
// New WebKit
[
'webkitRequestFullscreen',
'webkitExitFullscreen',
'webkitFullscreenElement',
'webkitFullscreenEnabled',
'webkitfullscreenchange',
'webkitfullscreenerror',
],
// Old WebKit
[
'webkitRequestFullScreen',
'webkitCancelFullScreen',
'webkitCurrentFullScreenElement',
'webkitCancelFullScreen',
'webkitfullscreenchange',
'webkitfullscreenerror',
],
[
'mozRequestFullScreen',
'mozCancelFullScreen',
'mozFullScreenElement',
'mozFullScreenEnabled',
'mozfullscreenchange',
'mozfullscreenerror',
],
[
'msRequestFullscreen',
'msExitFullscreen',
'msFullscreenElement',
'msFullscreenEnabled',
'MSFullscreenChange',
'MSFullscreenError',
],
];
let i = 0;
const l = fnMap.length;
const ret = {};
for (; i < l; i++) {
val = fnMap[i];
if (val && val[1] in document) {
for (i = 0; i < val.length; i++) {
ret[fnMap[0][i]] = val[i];
}
return ret;
}
}
return false;
})();
const eventNameMap = {
change: fn.fullscreenchange,
error: fn.fullscreenerror,
};
const screenfull = {
request: function (element, options) {
return new Promise(
function (resolve, reject) {
var onFullScreenEntered = function () {
this.off('change', onFullScreenEntered);
resolve();
}.bind(this);
this.on('change', onFullScreenEntered);
element = element || document.documentElement;
const returnPromise = element[fn.requestFullscreen](options);
if (returnPromise instanceof Promise) {
returnPromise.then(onFullScreenEntered).catch(reject);
}
}.bind(this)
);
},
exit: function () {
return new Promise(
function (resolve, reject) {
if (!this.isFullscreen) {
resolve();
return;
}
var onFullScreenExit = function () {
this.off('change', onFullScreenExit);
resolve();
}.bind(this);
this.on('change', onFullScreenExit);
const returnPromise = document[fn.exitFullscreen]();
if (returnPromise instanceof Promise) {
returnPromise.then(onFullScreenExit).catch(reject);
}
}.bind(this)
);
},
toggle: function (element, options) {
return this.isFullscreen ? this.exit() : this.request(element, options);
},
onchange: function (callback) {
this.on('change', callback);
},
onerror: function (callback) {
this.on('error', callback);
},
on: function (event, callback) {
const eventName = eventNameMap[event];
if (eventName) {
document.addEventListener(eventName, callback, false);
}
},
off: function (event, callback) {
const eventName = eventNameMap[event];
if (eventName) {
document.removeEventListener(eventName, callback, false);
}
},
raw: fn,
};
if (!fn) {
if (isCommonjs) {
module.exports = { isEnabled: false };
} else {
window.screenfull = { isEnabled: false };
}
return;
}
Object.defineProperties(screenfull, {
isFullscreen: {
get: function () {
return Boolean(document[fn.fullscreenElement]);
},
},
element: {
enumerable: true,
get: function () {
return document[fn.fullscreenElement];
},
},
isEnabled: {
enumerable: true,
get: function () {
// Coerce to boolean in case of old WebKit
return Boolean(document[fn.fullscreenEnabled]);
},
},
});
if (isCommonjs) {
module.exports = screenfull;
} else {
window.screenfull = screenfull;
}
})();
Fullscreen
Safari
- macOS safari
- iOS safari
Safari
- macOS safari
- iOS safari

const toggleFullscreen = useCallback(() => {
const vid = videoRef.current;
if (screenfull.isEnabled) {
if (screenfull.isFullscreen) {
screenfull.exit();
} else {
const target = targetRef.current;
if (isSafariBrowser) {
screenfull.request(target ? target : undefined);
} else {
screenfull.request();
}
}
} else {
if (vid && vid.webkitEnterFullscreen) {
onEnteriOSFullscreen();
vid.webkitEnterFullscreen();
}
}
}, [targetRef, onEnteriOSFullscreen, videoRef]);Autoplay System
怎麼做?
Todos
- detect the first video
- autoplay
Todos
- detect the first video
- autoplay
- pause previous playing video
Detect First Video
Detect First Video
Detect First Video
Detect First Video
Detect First Video
onScreen
Detect First Video
onScreen
onScreen
onScreen
onScreen
Detect First Video
export const getFirstEngagement = <T extends HTMLElement>(
container: T | null,
dataAttribute: string
): string | null => {
if (typeof document === 'undefined') return null;
const selector = `${scoped}[${dataAttribute}]:not([${dataAttribute}='false'])`;
const element = (container || document).querySelector(selector);
if (element) return element.getAttribute(dataAttribute);
return null;
};Detect First Video
onScreen
onScreen
onScreen
onScreen
Detect First Video
true
false
true
true
Autoplay
Autoplay
Current Playing Video
Global State
Autoplay
useEffect(() => {
// trigger if currentPlayingVideo changed and not ended
if (!ended) {
if (currentPlayingVideo === uniqAutoPlayId) {
setTimeout(() => {
onAutoPlay?.();
handlePlay();
}, 50);
} else {
handlePause();
}
}
});Linearize
Linearlize

Linearlize
A
B
Play A
Linearlize
A
B
Play A
Pause A
Linearlize
A
B
Play A
Pause A
Play B
Linearlize
A
B
Play A
Pause A
Play B
Pause B
Linearlize
A
B
Play B
Linearlize
A
B
Play B
Play A
Linearlize
A
B
Play B
Play A
Pause B
Linearlize
A
B
Play B
Play A
Pause B
Pause A
Autoplay
useEffect(() => {
// trigger if currentPlayingVideo changed and not ended
if (!ended) {
if (currentPlayingVideo === uniqAutoPlayId) {
setTimeout(() => {
onAutoPlay?.();
handlePlay();
}, 50);
} else {
handlePause();
}
}
});Solution
Atomic
VideoController
Play A
Queue
Pause A
Play B
Pause B
Play C
Pause C
Solution
Play A
Queue
Pause A
Play B
Pause B
Play C
Pause C
Atomic
VideoController
Solution
Play A
Queue
Pause A
Play B
Pause B
Play C
Pause C
Atomic
VideoController
Solution
Play A
Queue
Play B
Pause B
Play C
Pause C
Pause A
Play C
Atomic
VideoController
Solution
Play A
Queue
Play B
Pause B
Play C
Pause C
Pause A
Play C
Atomic
VideoController
Solution
Play A
Queue
Play B
Pause B
Play C
Pause C
Pause A
Play C
Atomic
VideoController
Solution
// handle atomic play video
const { handlePlay, handlePause } = useAtomicVideoController({
handlePlay: _handlePlay,
handlePause: _handlePause,
});
export default function useAtomicVideoController({
handlePlay,
handlePause,
}: {
handlePlay: () => void;
handlePause: () => void;
}) {
// handle atomic play video
const _handlePlay = useCallback(() => {
atomicVideoController.play({
play: handlePlay,
pause: handlePause,
});
}, [handlePlay, handlePause]);
return { handlePlay: _handlePlay, handlePause } as const;
}
Q&A
Thanks
deck
By 邱俊霖
deck
- 100