Transparent video read/write support (#145)

* Add support for reading transparent Matroska and implement alpha side data & decode

* Few fixes

* Implement alpha encoding

* Gracefully handle inability to acquire WebGL context, properly clean up WebGL contexts

* Improve touch device detection

* Test test

* Test test #2

* Test test 3

* Test test 4

* Test test 5

* Test test 6

* Test test 7

* Test test 8

* Test test 9

* Test test 10

* Test test 11

* Test test 12

* Test test 13

* Test test 14

* Test test 15

* Test test 16

* Test test 17

* Test test 18

* Test test 19

* Test test 20

* Fix type errors, improve MetadataTags docs

* Do a bunch of docs work

* Test test?

* "Unexpected only modifier 🤓"

* Add InputVideoTrack.canBeTransparent()

* Some clean-up

* Adjust CI to be less spammy in PRs

* Fix broken license headers
This commit is contained in:
David P.
2025-09-24 21:20:14 +02:00
committed by GitHub
parent 0878dd2ca9
commit fa4e064345
34 changed files with 1537 additions and 181 deletions
+12 -4
View File
@@ -35,7 +35,7 @@ const fullscreenButton = document.querySelector('#fullscreen-button') as HTMLBut
const errorElement = document.querySelector('#error-element') as HTMLDivElement;
const warningElement = document.querySelector('#warning-element') as HTMLDivElement;
const context = canvas.getContext('2d', { alpha: false, desynchronized: true })!;
const context = canvas.getContext('2d')!;
let audioContext: AudioContext | null = null;
let gainNode: GainNode | null = null;
@@ -149,11 +149,18 @@ const initMediaPlayer = async (resource: File | string) => {
gainNode.connect(audioContext.destination);
updateVolume();
const videoCanBeTransparent = videoTrack
? await videoTrack.canBeTransparent()
: false;
playerContainer.style.background = videoCanBeTransparent ? 'transparent' : '';
// For video, let's use a CanvasSink as it handles rotation and closing video samples for us.
// Pool size of 2: We'll only ever have the current and the next frame around, so we only need two canvases.
videoSink = videoTrack && new CanvasSink(videoTrack, {
poolSize: 2,
fit: 'contain', // In case the video changes dimensions over time
alpha: videoCanBeTransparent,
});
// For audio, we'll use an AudioBufferSink to directly retrieve AudioBuffers compatible with the Web Audio API
audioSink = audioTrack && new AudioBufferSink(audioTrack);
@@ -226,6 +233,7 @@ const startVideoIterator = async () => {
if (firstFrame) {
// Draw the first frame
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(firstFrame.canvas, 0, 0);
}
};
@@ -242,6 +250,7 @@ const render = (requestFrame = true) => {
// Check if the current playback time has caught up to the next frame
if (nextFrame && nextFrame.timestamp <= playbackTime) {
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(nextFrame.canvas, 0, 0);
nextFrame = null;
@@ -281,6 +290,7 @@ const updateNextFrame = async () => {
const playbackTime = getPlaybackTime();
if (newNextFrame.timestamp <= playbackTime) {
// Draw it immediately
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(newNextFrame.canvas, 0, 0);
} else {
// Save it for later
@@ -594,9 +604,7 @@ fullscreenButton.addEventListener('click', () => {
// I'm sorry for this
const isTouchDevice = () => {
return (('ontouchstart' in window)
|| (navigator.maxTouchPoints > 0)
|| ('msMaxTouchPoints' in navigator && (navigator.msMaxTouchPoints as number) > 0));
return 'ontouchstart' in window;
};
playerContainer.addEventListener('click', () => {
@@ -55,6 +55,7 @@ const extractMetadata = (resource: File | string) => {
'Coded width': `${track.codedWidth} pixels`,
'Coded height': `${track.codedHeight} pixels`,
'Rotation': `${track.rotation}° clockwise`,
'Transparency': track.canBeTransparent(),
}
: track.isAudioTrack()
? {