mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	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>
This commit is contained in:
		| @@ -1,2 +1,3 @@ | ||||
| export * from './open-api'; | ||||
| export * from './api'; | ||||
| export * from './utils'; | ||||
|   | ||||
							
								
								
									
										12
									
								
								web/src/api/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								web/src/api/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||
| } | ||||
| @@ -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' | ||||
| 				} | ||||
| 			); | ||||
| 		videoUrl = getFileUrl(assetInfo.deviceAssetId, assetInfo.deviceId, false, true); | ||||
|  | ||||
| 			if (!(data instanceof Blob)) { | ||||
| 				return; | ||||
| 			} | ||||
| 		return assetInfo; | ||||
| 	}; | ||||
|  | ||||
| 			const videoData = URL.createObjectURL(data); | ||||
| 			videoPlayerNode.src = videoData; | ||||
| 	const handleCanPlay = (ev: Event) => { | ||||
| 		const playerNode = ev.target as HTMLVideoElement; | ||||
|  | ||||
| 			videoPlayerNode.load(); | ||||
| 		playerNode.muted = true; | ||||
| 		playerNode.play(); | ||||
| 		playerNode.muted = false; | ||||
|  | ||||
| 			videoPlayerNode.oncanplay = () => { | ||||
| 				videoPlayerNode.muted = true; | ||||
| 				videoPlayerNode.play(); | ||||
| 				videoPlayerNode.muted = false; | ||||
|  | ||||
| 				isVideoLoading = 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> | ||||
|  | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user