5

<video id="video1" width="320" height="176" controls="controls">
  <source src="mov_bbb.mp4" type="video/mp4">
  
  <source src="mov_bbb.m4a" type="video/m4a">
  
</video>

I have a silent video and want to add his audio file in the video playback

  • 1
    Possible duplicate of [Get audio from HTML5 video](https://stackoverflow.com/questions/34984501/get-audio-from-html5-video) – Yannick K Apr 28 '19 at 09:13
  • 1
    not in a single element, you would need to combine the audio and video. You might be able to have a ` – Offbeatmammal Apr 28 '19 at 09:34
  • You should be able to use MediaSource Extensions to get what you need. – Brad Apr 29 '19 at 01:38
  • There's an answer in [another stackoverflow post](https://stackoverflow.com/questions/32313584/loading-a-mute-video-with-a-separate-audio-into-a-html5-video-tag). – Bei Jun 25 '20 at 12:54

2 Answers2

6

The MediaSource API allows us to do exactly this.

Note though that for this to work you need to prepare your media assets in a way it's compatible with that API. Have a thorough read of this MDN article which explains pretty well how to do so.

Once you have your assets, the remaining is quite straight-forward:

(async() => {

  const fetching = Promise.all( [
    // the video "only" file
    fetchData( "https://dl.dropboxusercontent.com/s/u9ycdfwy8fig4dl/bbb_video.mp4" ),
    // the audio "only" file
    fetchData( "https://dl.dropboxusercontent.com/s/rj4dh32vxwi1iv5/bbb_audio.mp4" )
  ] );

  const video_mime = "video/mp4; codecs=avc1.64000c";
  const audio_mime = "audio/mp4; codecs=mp4a.40.2";
  if(
    !MediaSource.isTypeSupported( video_mime ) ||
    !MediaSource.isTypeSupported( audio_mime )
  ) {
    throw "unsupported codecs";
  }
  
  const source = new MediaSource();
  document.querySelector( "video" ).src = URL.createObjectURL( source );
  await waitForEvent( source, "sourceopen" );

  const video_buffer = source.addSourceBuffer( video_mime );
  const audio_buffer = source.addSourceBuffer( audio_mime );
  video_buffer.mode = audio_buffer.mode = "sequence";

  const [ video_data, audio_data ] = await fetching;
  // There is a 'variable' limit as to how much
  // data we can append in on go, 10MB seems quite safe
  const chunk_size = 10 * 1024 * 1024;
  let i = 0;
  while (
    i < video_data.length &&
    i < audio_data.length
  ) {
    const next_i = i + chunk_size;
    const events = Promise.all( [
      waitForEvent( video_buffer, "updateend" ),
      waitForEvent( audio_buffer, "updateend" )
    ] );
    video_buffer.appendBuffer( video_data.subarray( i, next_i ) );
    audio_buffer.appendBuffer( audio_data.subarray( i, next_i ) );
    await events;
    i = next_i;
  }
  
  source.endOfStream();

})().catch( console.error );

function fetchData( url ) {
  return fetch( url )
    .then( (resp) => resp.ok && resp.arrayBuffer() )
    // we return an Uint8 view to be able to do a zero-cost subarray()
    .then( (buf) => new Uint8Array( buf ) );
}
function waitForEvent( target, event_name ) {
  return new Promise( (res) => {
    target.addEventListener( event_name, res, { once: true } );
  } );
}
<video controls></video>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Thanks. I tried this, but it failed because my files are local, and it seems `fetch` wont work with local files. Do you know any workaround for that? – Zombo May 10 '21 at 12:51
  • 1
    @StevenPenny The best would be to run local web server, you could also try using XMLHttpRequest instead of fetch, if your files are in the same directory as your document, otherwise you can start Chrome browser with `--allow-file-access-from-files`. – Kaiido May 10 '21 at 14:03
4

This works for me:

<meta charset="utf-8">
<video controls id="v" height="480" src="file.webm"></video>
<audio id="a" src="file.weba"></audio>

<script>
let v = document.getElementById('v');
let a = document.getElementById('a');
v.onpause = () => a.pause();
v.onplay = () => a.play();
v.onseeked = () => a.currentTime = v.currentTime;
</script>

Controls are on the video element, and audio element will respond to video play, pause and seek. The volume control doesn't work in this example, so you can either add that code, or just use the system volume control.

Zombo
  • 1
  • 62
  • 391
  • 407
  • 1
    HTMLMediaElement.play and setting HTMLMediaElement.currentTime are both async with low priority, meaning, you have a very high chance your audio won't be in sync with the video doing it like that. – Kaiido May 10 '21 at 05:33
  • 1
    Funny how the web seems to run in circles. This was a solved problem in [SMIL](https://www.w3.org/AudioVideo/) more than a decade ago. – Chris Wesseling May 10 '21 at 16:35