Просмотр исходного кода

fix(web): buffering for video player (#520)

* fix(web): buffering for video player

* chore(): missing file -_-

* refactor(web): using URL builder

* chore(): add semicolon

* fix(web): video player

* remove deadcode

Co-authored-by: Alex <alex.tran1502@gmail.com>
Thanh Pham 2 лет назад
Родитель
Сommit
fb0fa742f5

+ 1 - 0
web/src/api/index.ts

@@ -1,2 +1,3 @@
 export * from './open-api';
 export * from './api';
+export * from './utils';

+ 12 - 0
web/src/api/utils.ts

@@ -0,0 +1,12 @@
+let _basePath = '/api';
+
+export function getFileUrl(aid: string, did: string, isThumb?: boolean, isWeb?: boolean) {
+  const urlObj = new URL(`${window.location.origin}${_basePath}/asset/file`);
+  
+  urlObj.searchParams.append('aid', aid);
+  urlObj.searchParams.append('did', did);
+  if (isThumb !== undefined && isThumb !== null) urlObj.searchParams.append('isThumb', `${isThumb}`);
+  if (isWeb !== undefined && isWeb !== null) urlObj.searchParams.append('isWeb', `${isWeb}`);
+
+  return urlObj.href;
+}

+ 21 - 31
web/src/lib/components/asset-viewer/video-viewer.svelte

@@ -3,7 +3,7 @@
 
 	import { createEventDispatcher, onMount } from 'svelte';
 	import LoadingSpinner from '../shared-components/loading-spinner.svelte';
-	import { api, AssetResponseDto } from '@api';
+	import { api, AssetResponseDto, getFileUrl } from '@api';
 
 	export let assetId: string;
 
@@ -13,48 +13,32 @@
 
 	let videoPlayerNode: HTMLVideoElement;
 	let isVideoLoading = true;
+	let videoUrl: string;
 
 	onMount(async () => {
 		const { data: assetInfo } = await api.assetApi.getAssetById(assetId);
 
-		asset = assetInfo;
+		await loadVideoData(assetInfo);
 
-		await loadVideoData();
+		asset = assetInfo;
 	});
 
-	const loadVideoData = async () => {
+	const loadVideoData = async (assetInfo: AssetResponseDto) => {
 		isVideoLoading = true;
 
-		try {
-			const { data } = await api.assetApi.serveFile(
-				asset.deviceAssetId,
-				asset.deviceId,
-				false,
-				true,
-				{
-					responseType: 'blob'
-				}
-			);
-
-			if (!(data instanceof Blob)) {
-				return;
-			}
+		videoUrl = getFileUrl(assetInfo.deviceAssetId, assetInfo.deviceId, false, true);
 
-			const videoData = URL.createObjectURL(data);
-			videoPlayerNode.src = videoData;
-
-			videoPlayerNode.load();
+		return assetInfo;
+	};
 
-			videoPlayerNode.oncanplay = () => {
-				videoPlayerNode.muted = true;
-				videoPlayerNode.play();
-				videoPlayerNode.muted = false;
+	const handleCanPlay = (ev: Event) => {
+		const playerNode = ev.target as HTMLVideoElement;
 
-				isVideoLoading = false;
-			};
+		playerNode.muted = true;
+		playerNode.play();
+		playerNode.muted = false;
 
-			return videoData;
-		} catch (e) {}
+		isVideoLoading = false;
 	};
 </script>
 
@@ -63,7 +47,13 @@
 	class="flex place-items-center place-content-center h-full select-none"
 >
 	{#if asset}
-		<video controls class="h-full object-contain" bind:this={videoPlayerNode}>
+		<video
+			controls
+			class="h-full object-contain"
+			on:canplay={handleCanPlay}
+			bind:this={videoPlayerNode}
+		>
+			<source src={videoUrl} type="video/mp4" />
 			<track kind="captions" />
 		</video>
 

+ 33 - 64
web/src/lib/components/shared-components/immich-thumbnail.svelte

@@ -6,7 +6,7 @@
 	import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
 	import PauseCircleOutline from 'svelte-material-icons/PauseCircleOutline.svelte';
 	import LoadingSpinner from './loading-spinner.svelte';
-	import { api, AssetResponseDto, AssetTypeEnum, ThumbnailFormat } from '@api';
+	import { api, AssetResponseDto, AssetTypeEnum, getFileUrl, ThumbnailFormat } from '@api';
 
 	const dispatch = createEventDispatcher();
 
@@ -18,7 +18,7 @@
 	export let isExisted: boolean = false;
 
 	let imageData: string;
-	let videoData: string;
+	// let videoData: string;
 
 	let mouseOver: boolean = false;
 	$: dispatch('mouseEvent', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex });
@@ -28,7 +28,8 @@
 	let isThumbnailVideoPlaying = false;
 	let calculateVideoDurationIntervalHandler: NodeJS.Timer;
 	let videoProgress = '00:00';
-	let videoAbortController: AbortController;
+	// let videoAbortController: AbortController;
+	let videoUrl: string;
 
 	const loadImageData = async () => {
 		const { data } = await api.assetApi.getAssetThumbnail(asset.id, format, {
@@ -42,51 +43,8 @@
 
 	const loadVideoData = async () => {
 		isThumbnailVideoPlaying = false;
-		videoAbortController = new AbortController();
 
-		try {
-			const { data } = await api.assetApi.serveFile(
-				asset.deviceAssetId,
-				asset.deviceId,
-				false,
-				true,
-				{
-					responseType: 'blob',
-					signal: videoAbortController.signal
-				}
-			);
-
-			if (!(data instanceof Blob)) {
-				return;
-			}
-
-			videoData = URL.createObjectURL(data);
-
-			videoPlayerNode.src = videoData;
-
-			videoPlayerNode.load();
-
-			videoPlayerNode.onloadeddata = () => {
-				console.log('first frame load');
-			};
-
-			videoPlayerNode.oncanplaythrough = () => {
-				console.log('can play through');
-			};
-
-			videoPlayerNode.oncanplay = () => {
-				console.log('can play');
-				videoPlayerNode.muted = true;
-				videoPlayerNode.play();
-
-				isThumbnailVideoPlaying = true;
-				calculateVideoDurationIntervalHandler = setInterval(() => {
-					videoProgress = getVideoDurationInString(Math.round(videoPlayerNode.currentTime));
-				}, 1000);
-			};
-
-			return videoData;
-		} catch (e) {}
+		videoUrl = getFileUrl(asset.deviceAssetId, asset.deviceId, false, true);
 	};
 
 	const getVideoDurationInString = (currentTime: number) => {
@@ -136,12 +94,7 @@
 
 	const handleMouseLeaveThumbnail = () => {
 		mouseOver = false;
-
-		// Stop XHR download of video
-		videoAbortController?.abort();
-
-		// Stop video playback
-		URL.revokeObjectURL(videoData);
+		videoUrl = '';
 
 		clearInterval(calculateVideoDurationIntervalHandler);
 
@@ -149,6 +102,18 @@
 		videoProgress = '00:00';
 	};
 
+	const handleCanPlay = (ev: Event) => {
+		const playerNode = ev.target as HTMLVideoElement;
+
+		playerNode.muted = true;
+		playerNode.play();
+
+		isThumbnailVideoPlaying = true;
+		calculateVideoDurationIntervalHandler = setInterval(() => {
+			videoProgress = getVideoDurationInString(Math.round(playerNode.currentTime));
+		}, 1000);
+	};
+
 	$: getThumbnailBorderStyle = () => {
 		if (selected) {
 			return 'border-[20px] border-immich-primary/20';
@@ -259,17 +224,21 @@
 
 		{#if mouseOver && asset.type === AssetTypeEnum.Video}
 			<div class="absolute w-full h-full top-0" on:mouseenter={loadVideoData}>
-				<video
-					muted
-					autoplay
-					preload="none"
-					class="h-full object-cover"
-					width="250px"
-					style:width={`${thumbnailSize}px`}
-					bind:this={videoPlayerNode}
-				>
-					<track kind="captions" />
-				</video>
+				{#if videoUrl}
+					<video
+						muted
+						autoplay
+						preload="none"
+						class="h-full object-cover"
+						width="250px"
+						style:width={`${thumbnailSize}px`}
+						on:canplay={handleCanPlay}
+						bind:this={videoPlayerNode}
+					>
+						<source src={videoUrl} type="video/mp4" />
+						<track kind="captions" />
+					</video>
+				{/if}
 			</div>
 		{/if}
 	</div>