mirror of
https://github.com/linuxserver/pixelflux.git
synced 2026-02-19 16:27:45 +08:00
250 lines
13 KiB
HTML
250 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>H.264 Stripe Demo</title>
|
|
<style>
|
|
body { margin: 0; overflow: hidden; background-color: #333; }
|
|
canvas { display: block; } /* Ensures canvas is block-level, good practice */
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- The canvas element where the decoded video stripes will be drawn. -->
|
|
<canvas id="videoCanvas"></canvas>
|
|
|
|
<script>
|
|
// --- Configuration ---
|
|
const WEBSOCKET_URL = "ws://localhost:9000"; // WebSocket server URL
|
|
const DISPLAY_WIDTH = 1920; // Hardcoded display width for the demo
|
|
const DISPLAY_HEIGHT = 1080; // Hardcoded display height for the demo
|
|
// --- End Configuration ---
|
|
|
|
// Get the canvas element and its 2D rendering context.
|
|
const canvas = document.getElementById("videoCanvas");
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Set the canvas dimensions.
|
|
// The internal resolution of the canvas.
|
|
canvas.width = DISPLAY_WIDTH;
|
|
canvas.height = DISPLAY_HEIGHT;
|
|
|
|
// Optional: Style the canvas to fill the window if desired,
|
|
// or to be a fixed size. For this demo, it's fixed.
|
|
// canvas.style.width = "100vw";
|
|
// canvas.style.height = "100vh";
|
|
// canvas.style.objectFit = "contain"; // If scaling to window, maintain aspect ratio.
|
|
|
|
// This object will store VideoDecoder instances, one for each horizontal stripe.
|
|
// The key will be the stripe's Y-coordinate (stripe_y_start).
|
|
// The value will be an object: { decoder: VideoDecoder, pendingChunks: [] }
|
|
const stripeDecoders = {};
|
|
|
|
// Establish a WebSocket connection to the server.
|
|
const websocket = new WebSocket(WEBSOCKET_URL);
|
|
// Set the binary type to 'arraybuffer' to receive raw binary data.
|
|
websocket.binaryType = 'arraybuffer';
|
|
|
|
websocket.onopen = function(event) {
|
|
console.log("WebSocket connection opened to:", WEBSOCKET_URL);
|
|
// You could send an initial message to the server here if needed.
|
|
// For this demo, the server starts sending data upon connection.
|
|
};
|
|
|
|
websocket.onmessage = function(event) {
|
|
// `event.data` will be an ArrayBuffer containing the H.264 stripe data.
|
|
const receivedBuffer = event.data;
|
|
const dataView = new DataView(receivedBuffer);
|
|
|
|
// --- Parse the 10-byte Prefix ---
|
|
// Byte 0: DataType (should be 0x04 for H.264 stripe in this demo)
|
|
const dataType = dataView.getUint8(0);
|
|
|
|
if (dataType !== 0x04) {
|
|
console.warn(`Received unexpected data type: ${dataType}. Expected 0x04 for H.264 stripe.`);
|
|
return;
|
|
}
|
|
|
|
// Byte 1: Frame Type (0x01 for Key frame, 0x00 for Delta/P-frame)
|
|
const frameTypeByte = dataView.getUint8(1);
|
|
const chunkType = (frameTypeByte === 0x01) ? 'key' : 'delta';
|
|
|
|
// Bytes 2-3: Frame ID (unique ID for the full frame this stripe belongs to)
|
|
// Not strictly used for rendering in this minimal demo, but good to know it's there.
|
|
const frameId = dataView.getUint16(2, false); // false for big-endian
|
|
|
|
// Bytes 4-5: Stripe Y Start (vertical offset of this stripe)
|
|
const stripeYStart = dataView.getUint16(4, false); // false for big-endian
|
|
|
|
// Bytes 6-7: Stripe Width
|
|
const stripeWidth = dataView.getUint16(6, false); // false for big-endian
|
|
|
|
// Bytes 8-9: Stripe Height
|
|
const stripeHeight = dataView.getUint16(8, false); // false for big-endian
|
|
|
|
// The actual H.264 NALU data starts after the 10-byte prefix.
|
|
const h264NaluData = receivedBuffer.slice(10);
|
|
|
|
if (h264NaluData.byteLength === 0) {
|
|
console.warn(`Received empty H.264 NALU data for stripe Y=${stripeYStart}.`);
|
|
return;
|
|
}
|
|
// --- End Prefix Parsing ---
|
|
|
|
// Get or create the VideoDecoder for this specific stripe.
|
|
let decoderInfo = stripeDecoders[stripeYStart];
|
|
|
|
if (!decoderInfo) {
|
|
// If no decoder exists for this stripe's Y position, create one.
|
|
console.log(`Creating new VideoDecoder for stripe Y=${stripeYStart}, Width=${stripeWidth}, Height=${stripeHeight}`);
|
|
|
|
const newDecoder = new VideoDecoder({
|
|
output: (videoFrame) => {
|
|
// This is the output callback for the VideoDecoder.
|
|
// It's called when a frame has been successfully decoded.
|
|
try {
|
|
// Draw the decoded frame directly onto the canvas at its correct Y position.
|
|
ctx.drawImage(videoFrame, 0, stripeYStart);
|
|
} catch (e) {
|
|
console.error(`Error drawing decoded frame for stripe Y=${stripeYStart}:`, e);
|
|
} finally {
|
|
// IMPORTANT: Close the VideoFrame to release its resources.
|
|
videoFrame.close();
|
|
}
|
|
},
|
|
error: (e) => {
|
|
console.error(`VideoDecoder error for stripe Y=${stripeYStart}:`, e.message, e);
|
|
// More robust error handling might involve trying to reset or reconfigure the decoder.
|
|
// For this demo, we just log the error. If a decoder becomes permanently broken,
|
|
// its stripes might stop updating.
|
|
// Consider removing the decoder from stripeDecoders so it can be recreated on next keyframe.
|
|
if (stripeDecoders[stripeYStart] && stripeDecoders[stripeYStart].decoder === newDecoder) {
|
|
try {
|
|
if (newDecoder.state !== 'closed') newDecoder.close();
|
|
} catch (closeError) { /* ignore */ }
|
|
delete stripeDecoders[stripeYStart];
|
|
console.warn(`Removed faulty decoder for stripe Y=${stripeYStart}. It might be recreated on the next keyframe.`);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Configuration for the VideoDecoder.
|
|
const decoderConfig = {
|
|
codec: 'avc1.42E01E', // Common H.264 baseline codec string.
|
|
codedWidth: stripeWidth, // Width of the NALU data.
|
|
codedHeight: stripeHeight, // Height of the NALU data.
|
|
optimizeForLatency: true, // Prioritize faster decoding.
|
|
};
|
|
|
|
// Store the new decoder and a place for pending chunks.
|
|
stripeDecoders[stripeYStart] = { decoder: newDecoder, pendingChunks: [], config: decoderConfig };
|
|
decoderInfo = stripeDecoders[stripeYStart];
|
|
|
|
// Asynchronously check if the configuration is supported and then configure.
|
|
VideoDecoder.isConfigSupported(decoderConfig)
|
|
.then(support => {
|
|
if (support.supported) {
|
|
newDecoder.configure(decoderConfig);
|
|
console.log(`VideoDecoder configured for stripe Y=${stripeYStart}. Processing ${decoderInfo.pendingChunks.length} pending chunks.`);
|
|
// Process any chunks that arrived while the decoder was being set up.
|
|
processPendingChunks(stripeYStart);
|
|
} else {
|
|
console.error(`VideoDecoder configuration not supported for stripe Y=${stripeYStart}:`, decoderConfig, support);
|
|
// If config not supported, remove the placeholder.
|
|
if (stripeDecoders[stripeYStart] && stripeDecoders[stripeYStart].decoder === newDecoder) {
|
|
delete stripeDecoders[stripeYStart];
|
|
}
|
|
}
|
|
})
|
|
.catch(e => {
|
|
console.error(`Error during VideoDecoder support check or configure for stripe Y=${stripeYStart}:`, e);
|
|
if (stripeDecoders[stripeYStart] && stripeDecoders[stripeYStart].decoder === newDecoder) {
|
|
delete stripeDecoders[stripeYStart];
|
|
}
|
|
});
|
|
}
|
|
|
|
// At this point, decoderInfo should refer to the entry in stripeDecoders.
|
|
// Create an EncodedVideoChunk from the NALU data.
|
|
const chunkTimestamp = performance.now() * 1000; // Timestamps in microseconds.
|
|
// Must be monotonically increasing for a given decoder.
|
|
const encodedChunk = new EncodedVideoChunk({
|
|
type: chunkType,
|
|
timestamp: chunkTimestamp,
|
|
data: h264NaluData
|
|
});
|
|
|
|
// If the decoder is configured, decode immediately. Otherwise, queue the chunk.
|
|
if (decoderInfo.decoder.state === "configured") {
|
|
try {
|
|
decoderInfo.decoder.decode(encodedChunk);
|
|
} catch (e) {
|
|
console.error(`Error decoding chunk for stripe Y=${stripeYStart}:`, e, encodedChunk);
|
|
// If decode fails, consider resetting/re-creating this specific decoder.
|
|
}
|
|
} else if (decoderInfo.decoder.state === "unconfigured" || decoderInfo.decoder.state === "configuring") {
|
|
// console.log(`Decoder for stripe Y=${stripeYStart} is ${decoderInfo.decoder.state}. Queuing chunk.`);
|
|
decoderInfo.pendingChunks.push(encodedChunk);
|
|
} else { // 'closed'
|
|
console.warn(`Decoder for stripe Y=${stripeYStart} is closed. Chunk dropped. Frame ID: ${frameId}`);
|
|
// If it's closed, it might have errored. It could be recreated on the next keyframe.
|
|
}
|
|
};
|
|
|
|
function processPendingChunks(stripeY) {
|
|
const decoderInfo = stripeDecoders[stripeY];
|
|
if (!decoderInfo || decoderInfo.decoder.state !== "configured" || !decoderInfo.pendingChunks) {
|
|
return;
|
|
}
|
|
// console.log(`Processing ${decoderInfo.pendingChunks.length} pending chunks for stripe Y=${stripeY}`);
|
|
while (decoderInfo.pendingChunks.length > 0) {
|
|
const chunkToDecode = decoderInfo.pendingChunks.shift();
|
|
try {
|
|
decoderInfo.decoder.decode(chunkToDecode);
|
|
} catch (e) {
|
|
console.error(`Error decoding pending chunk for stripe Y=${stripeY}:`, e, chunkToDecode);
|
|
// If decode fails here, the decoder might be in a bad state.
|
|
}
|
|
}
|
|
}
|
|
|
|
websocket.onerror = function(error) {
|
|
console.error("WebSocket error:", error);
|
|
};
|
|
|
|
websocket.onclose = function(event) {
|
|
console.log("WebSocket connection closed. Code:", event.code, "Reason:", event.reason);
|
|
// Clean up all decoders when the WebSocket closes.
|
|
for (const yPos in stripeDecoders) {
|
|
if (stripeDecoders.hasOwnProperty(yPos)) {
|
|
const decoderInfo = stripeDecoders[yPos];
|
|
if (decoderInfo.decoder && decoderInfo.decoder.state !== "closed") {
|
|
try {
|
|
decoderInfo.decoder.close();
|
|
console.log(`Closed decoder for stripe Y=${yPos} on WebSocket close.`);
|
|
} catch (e) {
|
|
console.error(`Error closing decoder for stripe Y=${yPos} on WebSocket close:`, e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Clear the decoders object.
|
|
for (let key in stripeDecoders) delete stripeDecoders[key];
|
|
};
|
|
|
|
// No explicit requestAnimationFrame loop is needed for this version because
|
|
// VideoDecoder's `output` callback draws directly to the canvas when a frame is ready.
|
|
// This simplifies the rendering logic for a bare-minimum demo.
|
|
|
|
// Graceful shutdown: Close decoders if the window is closed/reloaded.
|
|
window.addEventListener('beforeunload', () => {
|
|
console.log("Window is closing. Cleaning up decoders...");
|
|
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
|
websocket.close(); // Attempt to close WebSocket cleanly.
|
|
}
|
|
// The websocket.onclose handler will then take care of closing individual decoders.
|
|
// If onclose doesn't fire in time, the browser will clean up, but this is good practice.
|
|
});
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|