mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 10:53:50 +02:00
Make media player AudioContext match audio track sample rate
This commit is contained in:
+4
-3
@@ -21,7 +21,7 @@
|
|||||||
chunked: true,
|
chunked: true,
|
||||||
chunkSize: 2**20
|
chunkSize: 2**20
|
||||||
});
|
});
|
||||||
const outputFormat = new Metamuxer.Mp4OutputFormat();
|
const outputFormat = new Metamuxer.WavOutputFormat();
|
||||||
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.textContent = 'Cancel';
|
button.textContent = 'Cancel';
|
||||||
@@ -38,8 +38,8 @@
|
|||||||
target
|
target
|
||||||
}),
|
}),
|
||||||
audio: {
|
audio: {
|
||||||
numberOfChannels: 2,
|
numberOfChannels: 1,
|
||||||
//sampleRate: 8000
|
sampleRate: 4000
|
||||||
//discard: true
|
//discard: true
|
||||||
//forceReencode: true,
|
//forceReencode: true,
|
||||||
},
|
},
|
||||||
@@ -68,6 +68,7 @@
|
|||||||
},
|
},
|
||||||
*/
|
*/
|
||||||
video: {
|
video: {
|
||||||
|
discard: true,
|
||||||
width: 1280,
|
width: 1280,
|
||||||
//discard: true,
|
//discard: true,
|
||||||
//width: 640
|
//width: 640
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ const fullscreenButton = document.querySelector('#fullscreen-button') as HTMLBut
|
|||||||
const errorElement = document.querySelector('#error-element') as HTMLDivElement;
|
const errorElement = document.querySelector('#error-element') as HTMLDivElement;
|
||||||
|
|
||||||
const context = canvas.getContext('2d')!;
|
const context = canvas.getContext('2d')!;
|
||||||
const audioContext = new AudioContext();
|
|
||||||
const gainNode = audioContext.createGain();
|
let audioContext: AudioContext | null = null;
|
||||||
gainNode.connect(audioContext.destination);
|
let gainNode: GainNode | null = null;
|
||||||
|
|
||||||
let fileLoaded = false;
|
let fileLoaded = false;
|
||||||
let videoSink: CanvasSink | null = null;
|
let videoSink: CanvasSink | null = null;
|
||||||
@@ -79,11 +79,6 @@ const initMediaPlayer = async (file: File) => {
|
|||||||
horizontalRule.style.display = '';
|
horizontalRule.style.display = '';
|
||||||
playerContainer.style.display = 'none';
|
playerContainer.style.display = 'none';
|
||||||
|
|
||||||
if (audioContext.state === 'suspended') {
|
|
||||||
// Resume the audio context now that a user gesture has happened
|
|
||||||
await audioContext.resume();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an Input from the file
|
// Create an Input from the file
|
||||||
const input = new Input({
|
const input = new Input({
|
||||||
source: new BlobSource(file),
|
source: new BlobSource(file),
|
||||||
@@ -110,6 +105,13 @@ const initMediaPlayer = async (file: File) => {
|
|||||||
throw new Error('Media file has no playable video or audio track.');
|
throw new Error('Media file has no playable video or audio track.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We must create the audio context with the matching sample rate for correct acoustic results
|
||||||
|
// (especially for low-sample rate files)
|
||||||
|
audioContext = new AudioContext({ sampleRate: audioTrack?.sampleRate });
|
||||||
|
gainNode = audioContext.createGain();
|
||||||
|
gainNode.connect(audioContext.destination);
|
||||||
|
updateVolume();
|
||||||
|
|
||||||
// For video, let's use a CanvasSink as it handles rotation and closing video samples for us.
|
// 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.
|
// 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 });
|
videoSink = videoTrack && new CanvasSink(videoTrack, { poolSize: 2 });
|
||||||
@@ -248,19 +250,19 @@ const runAudioIterator = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for await (const { buffer, timestamp } of audioBufferIterator!) {
|
for await (const { buffer, timestamp } of audioBufferIterator!) {
|
||||||
const node = audioContext.createBufferSource();
|
const node = audioContext!.createBufferSource();
|
||||||
node.buffer = buffer;
|
node.buffer = buffer;
|
||||||
node.connect(gainNode);
|
node.connect(gainNode!);
|
||||||
|
|
||||||
const startTimestamp = audioContextStartTime! + timestamp - playbackTimeAtStart;
|
const startTimestamp = audioContextStartTime! + timestamp - playbackTimeAtStart;
|
||||||
|
|
||||||
// Two cases: Either, the audio starts in the future or in the past
|
// Two cases: Either, the audio starts in the future or in the past
|
||||||
if (startTimestamp >= audioContext.currentTime) {
|
if (startTimestamp >= audioContext!.currentTime) {
|
||||||
// If the audio starts in the future, easy, we just schedule it
|
// If the audio starts in the future, easy, we just schedule it
|
||||||
node.start(startTimestamp);
|
node.start(startTimestamp);
|
||||||
} else {
|
} else {
|
||||||
// If it starts in the past, then let's only play the audible section that remains from here on out
|
// If it starts in the past, then let's only play the audible section that remains from here on out
|
||||||
node.start(audioContext.currentTime, audioContext.currentTime - startTimestamp);
|
node.start(audioContext!.currentTime, audioContext!.currentTime - startTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
queuedAudioNodes.add(node);
|
queuedAudioNodes.add(node);
|
||||||
@@ -290,7 +292,7 @@ const getPlaybackTime = () => {
|
|||||||
if (playing) {
|
if (playing) {
|
||||||
// To ensure perfect audio-video sync, we always use the audio context's clock to determine playback time, even
|
// To ensure perfect audio-video sync, we always use the audio context's clock to determine playback time, even
|
||||||
// when there is no audio track.
|
// when there is no audio track.
|
||||||
return audioContext.currentTime - audioContextStartTime! + playbackTimeAtStart;
|
return audioContext!.currentTime - audioContextStartTime! + playbackTimeAtStart;
|
||||||
} else {
|
} else {
|
||||||
return playbackTimeAtStart;
|
return playbackTimeAtStart;
|
||||||
}
|
}
|
||||||
@@ -303,7 +305,7 @@ const play = async () => {
|
|||||||
await startVideoIterator();
|
await startVideoIterator();
|
||||||
}
|
}
|
||||||
|
|
||||||
audioContextStartTime = audioContext.currentTime;
|
audioContextStartTime = audioContext!.currentTime;
|
||||||
playing = true;
|
playing = true;
|
||||||
|
|
||||||
if (audioSink) {
|
if (audioSink) {
|
||||||
@@ -398,7 +400,7 @@ const updateVolume = () => {
|
|||||||
const actualVolume = volumeMuted ? 0 : volume;
|
const actualVolume = volumeMuted ? 0 : volume;
|
||||||
|
|
||||||
volumeBar.style.width = `${actualVolume * 100}%`;
|
volumeBar.style.width = `${actualVolume * 100}%`;
|
||||||
gainNode.gain.value = actualVolume ** 2; // Quadratic for more fine-grained control
|
gainNode!.gain.value = actualVolume ** 2; // Quadratic for more fine-grained control
|
||||||
|
|
||||||
const iconNumber = volumeMuted ? 0 : Math.ceil(1 + 3 * volume);
|
const iconNumber = volumeMuted ? 0 : Math.ceil(1 + 3 * volume);
|
||||||
for (let i = 0; i < volumeIconWrapper.children.length; i++) {
|
for (let i = 0; i < volumeIconWrapper.children.length; i++) {
|
||||||
@@ -406,7 +408,6 @@ const updateVolume = () => {
|
|||||||
icon.style.display = i === iconNumber ? '' : 'none';
|
icon.style.display = i === iconNumber ? '' : 'none';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
updateVolume();
|
|
||||||
|
|
||||||
volumeBarContainer.addEventListener('pointerdown', (event) => {
|
volumeBarContainer.addEventListener('pointerdown', (event) => {
|
||||||
draggingVolumeBar = true;
|
draggingVolumeBar = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user