feat: video/gif partial support

This commit is contained in:
2026-02-04 12:37:40 +07:00
parent e1041ea845
commit 8b34e589bc
13 changed files with 1150 additions and 59 deletions
+113 -9
View File
@@ -7,9 +7,12 @@ import {
DEFAULT_BLE_CONFIG,
DEFAULT_PROTOCOL_CONFIG,
DEFAULT_IMAGE_CONFIG,
PacketType,
} from "../protocol/index.ts";
import { BleUploader } from "../ble/ble-client.ts";
import { ImageProcessor } from "../processing/image-processor.ts";
import { MediaDetector } from "../processing/media-detector.ts";
import { FrameExtractor } from "../processing/frame-extractor.ts";
import { PayloadBuilder } from "../protocol/index.ts";
import { logger } from "../utils/logger.ts";
import { UploadError } from "../utils/errors.ts";
@@ -18,7 +21,7 @@ export interface UploadOptions {
imagePath?: string;
imageData?: Buffer;
targetSize?: [number, number];
onProgress?: (progress: number) => void;
onProgress?: (progress: number, status?: string) => void;
}
/**
@@ -68,7 +71,12 @@ export class BeamBoxUploader {
}
/**
* Upload an image to the device
* Upload an image, GIF, or video to the device
*
* Automatically detects file type and uses appropriate upload method:
* - Static images: Type 6 (IMAGE) - single frame
* - Animated GIFs: Type 5 (DYNAMIC_AMBIENCE), frames extracted
* - Videos: Type 5 (DYNAMIC_AMBIENCE), frames extracted
*
* @param options Upload options
* @returns True if upload successful
@@ -82,7 +90,25 @@ export class BeamBoxUploader {
const effectiveSize = targetSize ?? this.imageConfig.defaultSize;
// Prepare JPEG data
// Detect file type if path provided
let isAnimated = false;
if (imagePath) {
const mediaInfo = await MediaDetector.detectFromFile(imagePath);
isAnimated = mediaInfo.type === "gif" || mediaInfo.type === "video";
logger.info(
`Detected file type: ${mediaInfo.type} (${mediaInfo.mimeType})`,
);
if (isAnimated) {
logger.info("File is animated, using Type 5 (DYNAMIC_AMBIENCE)");
return await this.uploadAnimation(imagePath, effectiveSize, onProgress);
} else {
logger.info("File is static image, using Type 6 (IMAGE)");
}
}
// Handle static image upload (Type 6)
let jpegData: Buffer;
if (imageData) {
jpegData = imageData;
@@ -100,7 +126,10 @@ export class BeamBoxUploader {
await this.sleep(1000);
// Step 1: Send image info packet to announce upload
const imageInfoPayload = this.payloadBuilder.buildImageInfo();
const imageInfoPayload = this.payloadBuilder.buildImageInfo(
PacketType.IMAGE,
1,
);
await this.ble.sendImageInfo(imageInfoPayload);
logger.info("Sent image info packet, proceeding to data transfer");
@@ -108,6 +137,7 @@ export class BeamBoxUploader {
const fullData = this.payloadBuilder.buildImageData(
jpegData,
effectiveSize,
PacketType.IMAGE,
);
const prefixLen = fullData.length - jpegData.length;
logger.info(
@@ -115,15 +145,15 @@ export class BeamBoxUploader {
);
// Send data in chunks with protocol packets
const ok = await this.ble.sendData(fullData, onProgress);
const ok = await this.ble.sendData(fullData, PacketType.IMAGE, onProgress);
if (!ok) {
logger.error("Upload reported error");
return false;
}
// Wait for final response
if (!(await this.ble.waitForResponse(5.0))) {
// Wait for all acknowledgments
if (!(await this.ble.waitForResponse(onProgress))) {
logger.error("Upload timeout waiting for response");
return false;
}
@@ -131,6 +161,80 @@ export class BeamBoxUploader {
return true;
}
/**
* Upload an animated GIF or video as Type 5 (DYNAMIC_AMBIENCE)
*
* @param filePath Path to the GIF or video file
* @param targetSize Target size for frames
* @param onProgress Progress callback
* @returns True if upload successful
*/
private async uploadAnimation(
filePath: string,
targetSize: [number, number],
onProgress?: (progress: number) => void,
): Promise<boolean> {
// Extract frames from the file
logger.info("Extracting frames from animation...");
const frames = await FrameExtractor.extractFrames(filePath, {
maxFrames: 100,
targetSize,
});
logger.info(`Extracted ${frames.length} frames`);
// Calculate frame interval
const intervalMs = await FrameExtractor.calculateFrameInterval(
filePath,
frames.length,
);
logger.info(`Using frame interval: ${intervalMs}ms`);
// Wait for device to be ready
logger.info("Waiting for device to be fully ready...");
await this.sleep(1000);
// Step 1: Send image info packet (Type 6 for the info)
const imageInfoPayload = this.payloadBuilder.buildImageInfo(
PacketType.IMAGE,
1,
);
await this.ble.sendImageInfo(imageInfoPayload);
logger.info("Sent animation info packet, proceeding to data transfer");
// Step 2: Build and send animation data payload (Type 5)
const fullData = this.payloadBuilder.buildAnimationData(
frames,
intervalMs,
targetSize,
);
logger.info(
`Animation payload bytes: total=${fullData.length}, frames=${frames.length}`,
);
// Send data in chunks with protocol packets using DYNAMIC_AMBIENCE packet type
const ok = await this.ble.sendData(
fullData,
PacketType.DYNAMIC_AMBIENCE,
onProgress,
);
if (!ok) {
logger.error("Animation upload reported error");
return false;
}
// Wait for all acknowledgments
if (!(await this.ble.waitForResponse(onProgress))) {
logger.error("Animation upload timeout waiting for response");
return false;
}
logger.info("Animation upload completed successfully");
return true;
}
/**
* Upload an image from a file
* @param imagePath Path to the image file
@@ -141,7 +245,7 @@ export class BeamBoxUploader {
public async uploadImageFromFile(
imagePath: string,
targetSize?: [number, number],
onProgress?: (progress: number) => void,
onProgress?: (progress: number, status?: string) => void,
): Promise<boolean> {
return await this.upload({ imagePath, targetSize, onProgress });
}
@@ -156,7 +260,7 @@ export class BeamBoxUploader {
public async uploadCheckerboard(
targetSize?: [number, number],
squares?: number,
onProgress?: (progress: number) => void,
onProgress?: (progress: number, status?: string) => void,
): Promise<boolean> {
const effectiveSize = targetSize ?? this.imageConfig.defaultSize;
const effectiveSquares = squares ?? this.imageConfig.checkerboardSquares;