mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-14 22:08:45 +00:00
비디오의 경우 원하는 장면으로 썸네일을 변경할 수 있도록 개선
This commit is contained in:
@@ -42,6 +42,30 @@ const generateImageThumbnail = (imageUrl: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const captureVideoThumbnail = (video: HTMLVideoElement) => {
|
||||||
|
return new Promise<Blob>((resolve, reject) => {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const { width, height } = scaleSize(video.videoWidth, video.videoHeight, 250);
|
||||||
|
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
if (!context) {
|
||||||
|
return reject(new Error("Failed to generate thumbnail"));
|
||||||
|
}
|
||||||
|
|
||||||
|
context.drawImage(video, 0, 0, width, height);
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (blob) {
|
||||||
|
resolve(blob);
|
||||||
|
} else {
|
||||||
|
reject(new Error("Failed to generate thumbnail"));
|
||||||
|
}
|
||||||
|
}, "image/webp");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const generateVideoThumbnail = (videoUrl: string, time = 0) => {
|
const generateVideoThumbnail = (videoUrl: string, time = 0) => {
|
||||||
return new Promise<Blob>((resolve, reject) => {
|
return new Promise<Blob>((resolve, reject) => {
|
||||||
const video = document.createElement("video");
|
const video = document.createElement("video");
|
||||||
@@ -49,25 +73,7 @@ const generateVideoThumbnail = (videoUrl: string, time = 0) => {
|
|||||||
video.currentTime = time;
|
video.currentTime = time;
|
||||||
};
|
};
|
||||||
video.onseeked = () => {
|
video.onseeked = () => {
|
||||||
const canvas = document.createElement("canvas");
|
captureVideoThumbnail(video).then(resolve).catch(reject);
|
||||||
const { width, height } = scaleSize(video.videoWidth, video.videoHeight, 250);
|
|
||||||
|
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
|
|
||||||
const context = canvas.getContext("2d");
|
|
||||||
if (!context) {
|
|
||||||
return reject(new Error("Failed to generate thumbnail"));
|
|
||||||
}
|
|
||||||
|
|
||||||
context.drawImage(video, 0, 0, width, height);
|
|
||||||
canvas.toBlob((blob) => {
|
|
||||||
if (blob) {
|
|
||||||
resolve(blob);
|
|
||||||
} else {
|
|
||||||
reject(new Error("Failed to generate thumbnail"));
|
|
||||||
}
|
|
||||||
}, "image/webp");
|
|
||||||
};
|
};
|
||||||
video.onerror = reject;
|
video.onerror = reject;
|
||||||
|
|
||||||
|
|||||||
@@ -11,15 +11,18 @@
|
|||||||
type FileInfo,
|
type FileInfo,
|
||||||
type CategoryInfo,
|
type CategoryInfo,
|
||||||
} from "$lib/modules/filesystem";
|
} from "$lib/modules/filesystem";
|
||||||
|
import { captureVideoThumbnail } from "$lib/modules/thumbnail";
|
||||||
import { fileDownloadStatusStore, isFileDownloading, masterKeyStore } from "$lib/stores";
|
import { fileDownloadStatusStore, isFileDownloading, masterKeyStore } from "$lib/stores";
|
||||||
import AddToCategoryBottomSheet from "./AddToCategoryBottomSheet.svelte";
|
import AddToCategoryBottomSheet from "./AddToCategoryBottomSheet.svelte";
|
||||||
import DownloadStatus from "./DownloadStatus.svelte";
|
import DownloadStatus from "./DownloadStatus.svelte";
|
||||||
import {
|
import {
|
||||||
requestFileRemovalFromCategory,
|
requestFileRemovalFromCategory,
|
||||||
requestFileDownload,
|
requestFileDownload,
|
||||||
|
requestThumbnailUpload,
|
||||||
requestFileAdditionToCategory,
|
requestFileAdditionToCategory,
|
||||||
} from "./service";
|
} from "./service";
|
||||||
|
|
||||||
|
import IconCamera from "~icons/material-symbols/camera";
|
||||||
import IconClose from "~icons/material-symbols/close";
|
import IconClose from "~icons/material-symbols/close";
|
||||||
import IconAddCircle from "~icons/material-symbols/add-circle";
|
import IconAddCircle from "~icons/material-symbols/add-circle";
|
||||||
|
|
||||||
@@ -40,6 +43,7 @@
|
|||||||
let isDownloadRequested = $state(false);
|
let isDownloadRequested = $state(false);
|
||||||
let viewerType: "image" | "video" | undefined = $state();
|
let viewerType: "image" | "video" | undefined = $state();
|
||||||
let fileBlobUrl: string | undefined = $state();
|
let fileBlobUrl: string | undefined = $state();
|
||||||
|
let videoElement: HTMLVideoElement | undefined = $state();
|
||||||
|
|
||||||
const updateViewer = async (buffer: ArrayBuffer, contentType: string) => {
|
const updateViewer = async (buffer: ArrayBuffer, contentType: string) => {
|
||||||
const fileBlob = new Blob([buffer], { type: contentType });
|
const fileBlob = new Blob([buffer], { type: contentType });
|
||||||
@@ -55,6 +59,11 @@
|
|||||||
return fileBlob;
|
return fileBlob;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateThumbnail = async (dataKey: CryptoKey, dataKeyVersion: Date) => {
|
||||||
|
const thumbnail = await captureVideoThumbnail(videoElement!);
|
||||||
|
await requestThumbnailUpload(data.id, thumbnail, dataKey, dataKeyVersion);
|
||||||
|
};
|
||||||
|
|
||||||
const addToCategory = async (categoryId: number) => {
|
const addToCategory = async (categoryId: number) => {
|
||||||
await requestFileAdditionToCategory(data.id, categoryId);
|
await requestFileAdditionToCategory(data.id, categoryId);
|
||||||
isAddToCategoryBottomSheetOpen = false;
|
isAddToCategoryBottomSheetOpen = false;
|
||||||
@@ -133,8 +142,17 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{:else if viewerType === "video"}
|
{:else if viewerType === "video"}
|
||||||
{#if fileBlobUrl}
|
{#if fileBlobUrl}
|
||||||
<!-- svelte-ignore a11y_media_has_caption -->
|
<div class="flex flex-col space-y-2">
|
||||||
<video src={fileBlobUrl} controls></video>
|
<!-- svelte-ignore a11y_media_has_caption -->
|
||||||
|
<video bind:this={videoElement} src={fileBlobUrl} controls muted></video>
|
||||||
|
<IconEntryButton
|
||||||
|
icon={IconCamera}
|
||||||
|
onclick={() => updateThumbnail($info.dataKey!, $info.dataKeyVersion!)}
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
이 장면을 썸네일로 설정하기
|
||||||
|
</IconEntryButton>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{@render viewerLoading("비디오를 불러오고 있어요.")}
|
{@render viewerLoading("비디오를 불러오고 있어요.")}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,9 +1,37 @@
|
|||||||
import { callPostApi } from "$lib/hooks";
|
import { callPostApi } from "$lib/hooks";
|
||||||
import type { CategoryFileAddRequest } from "$lib/server/schemas";
|
import { encryptData } from "$lib/modules/crypto";
|
||||||
|
import { storeFileThumbnailCache } from "$lib/modules/file";
|
||||||
|
import type { CategoryFileAddRequest, FileThumbnailUploadRequest } from "$lib/server/schemas";
|
||||||
|
|
||||||
export { requestCategoryCreation, requestFileRemovalFromCategory } from "$lib/services/category";
|
export { requestCategoryCreation, requestFileRemovalFromCategory } from "$lib/services/category";
|
||||||
export { requestFileDownload } from "$lib/services/file";
|
export { requestFileDownload } from "$lib/services/file";
|
||||||
|
|
||||||
|
export const requestThumbnailUpload = async (
|
||||||
|
fileId: number,
|
||||||
|
thumbnail: Blob,
|
||||||
|
dataKey: CryptoKey,
|
||||||
|
dataKeyVersion: Date,
|
||||||
|
) => {
|
||||||
|
const thumbnailBuffer = await thumbnail.arrayBuffer();
|
||||||
|
const thumbnailEncrypted = await encryptData(thumbnailBuffer, dataKey);
|
||||||
|
|
||||||
|
const form = new FormData();
|
||||||
|
form.set(
|
||||||
|
"metadata",
|
||||||
|
JSON.stringify({
|
||||||
|
dekVersion: dataKeyVersion.toISOString(),
|
||||||
|
contentIv: thumbnailEncrypted.iv,
|
||||||
|
} satisfies FileThumbnailUploadRequest),
|
||||||
|
);
|
||||||
|
form.set("content", new Blob([thumbnailEncrypted.ciphertext]));
|
||||||
|
|
||||||
|
const res = await fetch(`/api/file/${fileId}/thumbnail/upload`, { method: "POST", body: form });
|
||||||
|
if (!res.ok) return false;
|
||||||
|
|
||||||
|
storeFileThumbnailCache(fileId, thumbnailBuffer); // Intended
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
export const requestFileAdditionToCategory = async (fileId: number, categoryId: number) => {
|
export const requestFileAdditionToCategory = async (fileId: number, categoryId: number) => {
|
||||||
const res = await callPostApi<CategoryFileAddRequest>(`/api/category/${categoryId}/file/add`, {
|
const res = await callPostApi<CategoryFileAddRequest>(`/api/category/${categoryId}/file/add`, {
|
||||||
file: fileId,
|
file: fileId,
|
||||||
|
|||||||
Reference in New Issue
Block a user