array
This commit is contained in:
@@ -129,6 +129,11 @@ export const ffmpegConvertToMP4 = async (
|
||||
await execAsync(cmd);
|
||||
};
|
||||
|
||||
export interface FFmpegGenerateHLSPlaylistAndSegmentsResult {
|
||||
playlistPath: string;
|
||||
videoPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A bespoke variant of {@link ffmpegExec} for generation of HLS playlists for
|
||||
* videos.
|
||||
@@ -138,17 +143,18 @@ export const ffmpegConvertToMP4 = async (
|
||||
* @param inputFilePath The path to a file on the user's local file system. This
|
||||
* is the video we want to convert.
|
||||
*
|
||||
* @param outputPlaylistPath The path to a file on the user's local file system
|
||||
* where we should write the generated HLS playlist.
|
||||
* @param outputPathPrefix The path to unique and temporary prefix on the user's
|
||||
* local file system - we should write the generated HLS playlist and video
|
||||
* segments using this prefix and adding the necessary suffixes.
|
||||
*
|
||||
* @param outputVideoPath The path to a file on the user's local file system
|
||||
* where we should write the transcoded and encrypted video that the HLS
|
||||
* playlist refers to.
|
||||
* @returns The paths to two files on the user's local file system - one
|
||||
* containing the generated HLS playlist, and the other containing the
|
||||
* transcoded and encrypted video segments that the HLS playlist refers to.
|
||||
*/
|
||||
export const ffmpegGenerateHLSPlaylist = async (
|
||||
export const ffmpegGenerateHLSPlaylistAndSegments = async (
|
||||
inputFilePath: string,
|
||||
outputFilePath: string,
|
||||
): Promise<void> => {
|
||||
): Promise<FFmpegGenerateHLSPlaylistAndSegmentsResult> => {
|
||||
// Current parameters
|
||||
//
|
||||
// - H264
|
||||
@@ -186,4 +192,6 @@ export const ffmpegGenerateHLSPlaylist = async (
|
||||
const cmd = substitutePlaceholders(command, inputFilePath, outputFilePath);
|
||||
|
||||
await execAsync(cmd);
|
||||
|
||||
throw new Error("TODO(HLS)");
|
||||
};
|
||||
|
||||
@@ -7,7 +7,11 @@ import fs from "node:fs/promises";
|
||||
import { Writable } from "node:stream";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import log from "./log";
|
||||
import { ffmpegConvertToMP4 } from "./services/ffmpeg";
|
||||
import {
|
||||
ffmpegConvertToMP4,
|
||||
ffmpegGenerateHLSPlaylistAndSegments,
|
||||
type FFmpegGenerateHLSPlaylistAndSegmentsResult,
|
||||
} from "./services/ffmpeg";
|
||||
import { markClosableZip, openZip } from "./services/zip";
|
||||
import { writeStream } from "./utils/stream";
|
||||
import {
|
||||
@@ -74,7 +78,7 @@ const handleStreamRequest = async (request: Request): Promise<Response> => {
|
||||
case "convert-to-mp4":
|
||||
return handleConvertToMP4Write(request);
|
||||
case "generate-hls":
|
||||
return new Response("TODO", { status: 400 });
|
||||
return handleGenerateHLSWrite(request);
|
||||
default:
|
||||
return new Response(`Unknown op ${op}`, {
|
||||
status: 404,
|
||||
@@ -210,7 +214,7 @@ export const clearPendingVideoResults = () => pendingVideoResults.clear();
|
||||
*
|
||||
* renderer → main stream://video?op=convert-to-mp4
|
||||
* → request.body is the original video
|
||||
* ← response is a token
|
||||
* ← response is [token]
|
||||
*
|
||||
* renderer → main stream://video?token=<token>
|
||||
* ← response.body is the converted video
|
||||
@@ -242,7 +246,7 @@ const handleConvertToMP4Write = async (request: Request) => {
|
||||
|
||||
const token = randomUUID();
|
||||
pendingVideoResults.set(token, outputTempFilePath);
|
||||
return new Response(token, { status: 200 });
|
||||
return new Response(JSON.stringify([token]), { status: 200 });
|
||||
};
|
||||
|
||||
const handleVideoRead = async (token: string) => {
|
||||
@@ -263,3 +267,42 @@ const handleVideoDone = async (token: string) => {
|
||||
pendingVideoResults.delete(token);
|
||||
return new Response("", { status: 200 });
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a HLS playlist for the given video.
|
||||
*
|
||||
* See: [Note: Convert to MP4] for the general architecture of commands that do
|
||||
* renderer <-> main I/O using streams.
|
||||
*
|
||||
* The difference here is that we the conversion generates two streams - one for
|
||||
* the HLS playlist itself, and one for the file containing the encrypted and
|
||||
* transcoded video chunks. So instead of returning a single token, we return a
|
||||
* JSON array containing two tokens so that the renderer can read them off
|
||||
* separately.
|
||||
*/
|
||||
const handleGenerateHLSWrite = async (request: Request) => {
|
||||
const inputTempFilePath = await makeTempFilePath();
|
||||
await writeStream(inputTempFilePath, request.body!);
|
||||
|
||||
const outputFilePathPrefix = await makeTempFilePath();
|
||||
let paths: FFmpegGenerateHLSPlaylistAndSegmentsResult;
|
||||
try {
|
||||
paths = await ffmpegGenerateHLSPlaylistAndSegments(
|
||||
inputTempFilePath,
|
||||
outputFilePathPrefix,
|
||||
);
|
||||
} catch (e) {
|
||||
log.error("Generate HLS failed", e);
|
||||
throw e;
|
||||
} finally {
|
||||
await deleteTempFileIgnoringErrors(inputTempFilePath);
|
||||
}
|
||||
|
||||
const playlistToken = randomUUID();
|
||||
const videoToken = randomUUID();
|
||||
pendingVideoResults.set(playlistToken, paths.playlistPath);
|
||||
pendingVideoResults.set(videoToken, paths.videoPath);
|
||||
return new Response(JSON.stringify([playlistToken, videoToken]), {
|
||||
status: 200,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -266,7 +266,8 @@ export const convertToMP4 = async (blob: Blob): Promise<Blob | Uint8Array> => {
|
||||
};
|
||||
|
||||
const convertToMP4Native = async (electron: Electron, blob: Blob) => {
|
||||
const token = await writeVideoStream(electron, "convert-to-mp4", blob);
|
||||
const tokens = await writeVideoStream(electron, "convert-to-mp4", blob);
|
||||
const token = tokens[0]!;
|
||||
const mp4Blob = await readVideoStream(electron, token);
|
||||
await videoStreamDone(electron, token);
|
||||
return mp4Blob;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import type { Electron, ElectronMLWorker, ZipItem } from "ente-base/types/ipc";
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Stream the given file or zip entry from the user's local file system.
|
||||
@@ -137,14 +138,22 @@ type VideoStreamOp = "convert-to-mp4" | "generate-hls";
|
||||
* @param video The video to convert, as a {@link Blob} or a
|
||||
* {@link ReadableStream}.
|
||||
*
|
||||
* @returns a token that can then be passed to {@link readVideoStream} to read
|
||||
* back the processed video.
|
||||
* @returns an array of token that can then be passed to {@link readVideoStream}
|
||||
* to read back the processed video. The count (and semantics) of the tokens are
|
||||
* dependent on the operation:
|
||||
*
|
||||
* - "convert-to-mp4" returns a single token (which can be used to retrieve the
|
||||
* converted MP4 file).
|
||||
*
|
||||
* - "generate-hls" returns two tokens, first one that can be used to retrieve
|
||||
* the generated HLS playlist, and the second one that can be used to retrieve
|
||||
* the video (segments).
|
||||
*/
|
||||
export const writeVideoStream = async (
|
||||
_: Electron,
|
||||
op: VideoStreamOp,
|
||||
video: Blob | ReadableStream,
|
||||
) => {
|
||||
): Promise<string[]> => {
|
||||
const url = `stream://video?op=${op}`;
|
||||
|
||||
const req = new Request(url, {
|
||||
@@ -160,8 +169,7 @@ export const writeVideoStream = async (
|
||||
if (!res.ok)
|
||||
throw new Error(`Failed to write stream to ${url}: HTTP ${res.status}`);
|
||||
|
||||
const token = res.text();
|
||||
return token;
|
||||
return z.array(z.string()).parse(await res.json());
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user