memos/web/src/utils/media-item.ts

180 lines
5.2 KiB
TypeScript

import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import {
getAttachmentMotionClipUrl,
getAttachmentMotionGroupId,
getAttachmentThumbnailUrl,
getAttachmentType,
getAttachmentUrl,
isAndroidMotionContainer,
isAppleLivePhotoStill,
isAppleLivePhotoVideo,
isMotionAttachment,
} from "./attachment";
export interface PreviewMediaItem {
id: string;
kind: "image" | "video";
sourceUrl: string;
posterUrl?: string;
filename: string;
isMotion: boolean;
presentationTimestampUs?: bigint;
}
export interface AttachmentVisualItem {
id: string;
kind: "image" | "video" | "motion";
filename: string;
posterUrl: string;
sourceUrl: string;
attachmentNames: string[];
attachments: Attachment[];
previewItem: PreviewMediaItem;
mimeType: string;
}
export function buildAttachmentVisualItems(attachments: Attachment[]): AttachmentVisualItem[] {
const attachmentsByGroup = new Map<string, Attachment[]>();
for (const attachment of attachments) {
const groupId = getAttachmentMotionGroupId(attachment);
if (!groupId) {
continue;
}
const group = attachmentsByGroup.get(groupId) ?? [];
group.push(attachment);
attachmentsByGroup.set(groupId, group);
}
const consumedGroups = new Set<string>();
const items: AttachmentVisualItem[] = [];
for (const attachment of attachments) {
if (isAndroidMotionContainer(attachment)) {
items.push(buildAndroidMotionItem(attachment));
continue;
}
const groupId = getAttachmentMotionGroupId(attachment);
if (!groupId || consumedGroups.has(groupId)) {
if (!groupId) {
items.push(buildSingleAttachmentItem(attachment));
}
continue;
}
const group = attachmentsByGroup.get(groupId) ?? [];
const still = group.find(isAppleLivePhotoStill);
const video = group.find(isAppleLivePhotoVideo);
if (still && video && group.length === 2) {
items.push(buildAppleMotionItem(still, video));
consumedGroups.add(groupId);
continue;
}
items.push(buildSingleAttachmentItem(attachment));
consumedGroups.add(groupId);
for (const member of group) {
if (member.name === attachment.name) {
continue;
}
items.push(buildSingleAttachmentItem(member));
}
}
return dedupeVisualItems(items);
}
export function countLogicalAttachmentItems(attachments: Attachment[]): number {
const visualAttachments = attachments.filter(
(attachment) =>
getAttachmentType(attachment) === "image/*" || getAttachmentType(attachment) === "video/*" || isMotionAttachment(attachment),
);
const visualNames = new Set(visualAttachments.map((attachment) => attachment.name));
const visualCount = buildAttachmentVisualItems(visualAttachments).length;
const nonVisualCount = attachments.filter((attachment) => !visualNames.has(attachment.name)).length;
return visualCount + nonVisualCount;
}
function buildSingleAttachmentItem(attachment: Attachment): AttachmentVisualItem {
const attachmentType = getAttachmentType(attachment);
const sourceUrl = getAttachmentUrl(attachment);
const posterUrl = attachmentType === "image/*" ? getAttachmentThumbnailUrl(attachment) : sourceUrl;
const previewKind = attachmentType === "video/*" ? "video" : "image";
return {
id: attachment.name,
kind: attachmentType === "video/*" ? "video" : "image",
filename: attachment.filename,
posterUrl,
sourceUrl,
attachmentNames: [attachment.name],
attachments: [attachment],
previewItem: {
id: attachment.name,
kind: previewKind,
sourceUrl,
posterUrl,
filename: attachment.filename,
isMotion: false,
},
mimeType: attachment.type,
};
}
function buildAppleMotionItem(still: Attachment, video: Attachment): AttachmentVisualItem {
const sourceUrl = getAttachmentUrl(video);
const posterUrl = getAttachmentThumbnailUrl(still);
return {
id: getAttachmentMotionGroupId(still) ?? still.name,
kind: "motion",
filename: still.filename,
posterUrl,
sourceUrl,
attachmentNames: [still.name, video.name],
attachments: [still, video],
previewItem: {
id: getAttachmentMotionGroupId(still) ?? still.name,
kind: "video",
sourceUrl,
posterUrl,
filename: still.filename,
isMotion: true,
},
mimeType: still.type,
};
}
function buildAndroidMotionItem(attachment: Attachment): AttachmentVisualItem {
return {
id: attachment.name,
kind: "motion",
filename: attachment.filename,
posterUrl: getAttachmentThumbnailUrl(attachment),
sourceUrl: getAttachmentMotionClipUrl(attachment),
attachmentNames: [attachment.name],
attachments: [attachment],
previewItem: {
id: attachment.name,
kind: "video",
sourceUrl: getAttachmentMotionClipUrl(attachment),
posterUrl: getAttachmentThumbnailUrl(attachment),
filename: attachment.filename,
isMotion: true,
presentationTimestampUs: attachment.motionMedia?.presentationTimestampUs,
},
mimeType: attachment.type,
};
}
function dedupeVisualItems(items: AttachmentVisualItem[]): AttachmentVisualItem[] {
const seen = new Set<string>();
return items.filter((item) => {
if (seen.has(item.id)) {
return false;
}
seen.add(item.id);
return true;
});
}