Video export with MediaMuxer

I have this code to export frames to video and it works fine, but I don’t quite understand how I can add audio.

public class VideoExport {
  
  private MediaMuxer muxer;
  private MediaCodec codec;
  private MediaCodec.BufferInfo bufferInfo;
  private int trackIndex;
  private boolean muxerStarted;
  private int frameCount;
  private int fps;
  private boolean recording;
  private String savePath,fileName;
  private int w,h;

  public VideoExport() {
    trackIndex=-1;
    muxerStarted=false;
    frameCount=0;
    fps=24;
    recording=false;
    savePath="/storage/emulated/0/Movies/";
    fileName="video";
    w=100;
    h=100;
  }

  void setSize(int w,int h) {
    this.w=(w<2)?2:w-(w%2);
    this.h=(h<2)?2:h-(h%2);
  }

  void setName(String fileName) {
    this.fileName=fileName;
  }

  void setSavePath(String savePath) {
    this.savePath=savePath;
  }

  void setFps(int fps) {
    this.fps=fps;
  }

  public void startRecording(){
    if(!recording){
      recording=true;
      frameCount=0;
      setupMediaMuxer();
    }
    else{
      println();
      println("can't start recording while recording");
    }
  }

  void setupMediaMuxer() {
    try {
      // Настройка файла для сохранения
      File outputFile = new File(savePath+fileName+".mp4");
      muxer=new MediaMuxer(outputFile.getAbsolutePath(),
      MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

      // Настройка формата видео
      MediaFormat format=
      MediaFormat.createVideoFormat("video/avc",w,h);
      format.setInteger(MediaFormat.KEY_COLOR_FORMAT,21); 
      format.setInteger(MediaFormat.KEY_BIT_RATE,10000000);
      format.setInteger(MediaFormat.KEY_FRAME_RATE,fps);
      format.setString(MediaFormat.KEY_PROFILE,"Main");
      format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,1);

      // Создание и настройка кодека для H.264
      codec = MediaCodec.createEncoderByType("video/avc");
      codec.configure(format,null,null,
      MediaCodec.CONFIGURE_FLAG_ENCODE);
      codec.start();

      bufferInfo = new MediaCodec.BufferInfo();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  void encodeFrame(PImage frame) {
    try {
      // Подготовка данных для кадра
      int inputBufferIndex=codec.dequeueInputBuffer(10000);
      if(inputBufferIndex>=0) {
        ByteBuffer inputBuffer=
        codec.getInputBuffer(inputBufferIndex);
        inputBuffer.clear();
        inputBuffer.put(getFrameData(frame));
        codec.queueInputBuffer(inputBufferIndex,0,
        inputBuffer.limit(),frameCount*1000000/fps,0);
      }

      // Запись кадра в выходное видео
      int outputBufferIndex=
      codec.dequeueOutputBuffer(bufferInfo,10000);
      while(outputBufferIndex>=0) {
        ByteBuffer outputBuffer=
        codec.getOutputBuffer(outputBufferIndex);

        if(!muxerStarted) {
          MediaFormat newFormat=codec.getOutputFormat();
          trackIndex=muxer.addTrack(newFormat);
          muxer.start();
          muxerStarted=true;
        }

        muxer.writeSampleData(
        trackIndex,outputBuffer,bufferInfo);
        codec.releaseOutputBuffer(outputBufferIndex,false);
        outputBufferIndex=
        codec.dequeueOutputBuffer(bufferInfo,10000);
      }
      frameCount++;
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public void stopRecording(){
    if(recording){
      codec.stop();
      codec.release();
      muxer.stop();
      muxer.release();
      recording=false;
    }
    else{
      println();
      println("Can't stop recording before it has started.");
    }
  }

  ByteBuffer getFrameData(PImage frame) {
    frame.loadPixels();
    ByteBuffer buffer = ByteBuffer.allocate(w*h*3/2);
    int yIndex=0;
    int uvIndex=w*h;

    for(int j=0;j<h;j++) {
      for(int i=0;i<w;i++) {
        int argb=frame.pixels[j*w+i];

        int r=(argb>>16)&0xFF;
        int g=(argb>>8)&0xFF;
        int b=argb&0xFF;

        int y=(int)(0.257*r+0.504*g+0.098*b+16);
        int u=(int)(-0.148*r-0.291*g+0.439*b+128);
        int v=(int)(0.439*r-0.368*g-0.071*b+128);

        buffer.put(yIndex++,(byte)(y&0xFF));

        if(j%2==0&&i%2==0) {
          buffer.put(uvIndex++,(byte)(u&0xFF));
          buffer.put(uvIndex++,(byte)(v&0xFF));
        }
      }
    }
    return buffer;
  }

  public boolean isRecording() {
    return recording;
  }
}

Example of using:

import android.media.*;
import java.io.*;
import java.nio.*;
import java.io.File;




VideoExport vid;

void setup(){
  size(720,720);
  
  
  vid = new VideoExport();
  vid.setSize(720,720);
  vid.setName("my video");
  vid.setFps(12);
  vid.startRecording();
}

void draw() {
  background(255);
  fill(255,0,0);
  noStroke();
  ellipse(width/2,height/2+frameCount,100,100);
  
  fill(150);
  text(vid.isRecording()+"",100,100);

  // Запись кадра
  if(frameCount<=48){
    vid.encodeFrame(get());
  }
  else if(frameCount==48+1){
    vid.stopRecording();
  }
}

I don’t know much about MediaMuxer, I tried to add audio to muxer with MediaExtractor but I get errors. And so far I’m stuck at this point. I would be very grateful if you can help me!

I tried the video export code that you posted and it seems to work as advertised as far as creating an .mp4 video. However, I don’t see any code for AudioFormat. I’m also wondering if you don’t need to play a sound file along with your animation. The way it is now you have no sound to record. It might also help to post the code that you’ve tried to add along with the error messages and perhaps we can figure it out as we go along. I have not done this previously but am willing to try and help. This reference may or may not be helpful: android - How to mux audio file and video file? - Stack Overflow

Another reference if you haven’t already seen it:

Thank you for your response. I want to make it clear that this is not really my code. This is the case when I just copied something and tried to paste it into my program. So I don’t really understand how MediaMuxer works.

Here is the code with my attempt to add audio to a video:

import android.media.*;
import java.io.*;
import java.nio.*;

public class VideoExport {  
  private MediaMuxer muxer;  
  private MediaCodec codec;  

  private MediaCodec.BufferInfo bufferInfo;  
  private int videoTrackIndex,audioTrackIndex;  
  private boolean muxerStarted;  
  private int frameCount,fps;  
  private boolean recording;
  private boolean audioWriten;
  private String savePath,fileName,audioPath;  
  private int w,h;  
  private MediaExtractor audioExtractor;
  private boolean hasAudio;
  private int audioStartTime,audioEndTime;

  public VideoExport() {  
    videoTrackIndex=-1;  
    audioTrackIndex=-1;  
    muxerStarted=false;  
    frameCount=0;  
    fps=24;  
    recording=false;  
    savePath="/storage/emulated/0/Movies/";  
    fileName="video";  
    w=100;  
    h=100;
    hasAudio=false;
  }  

  void setSize(int w,int h) {  
    this.w=(w<2)?2:w-(w%2);  
    this.h=(h<2)?2:h-(h%2);  
  }  

  void setName(String fileName) {  
    this.fileName=fileName;  
  }  

  void setSavePath(String savePath) {  
    this.savePath=savePath;  
  }  

  void setFps(int fps) {  
    this.fps=fps;  
  }  

  public void addAudio(String audioPath,int startTime,int endTime) {
    this.audioPath=audioPath;
    this.audioStartTime=startTime*1000;
    this.audioEndTime=endTime*1000;
    hasAudio=true;
  }

  public void startRecording() {  
    if(!recording) {  
      recording=true;  
      frameCount=0;  
      setupMediaMuxer();  
    } else {  
      println("Can't start recording while recording");  
    }  
  }  

  void setupMediaMuxer() {
    try {
      File outputFile=new File(savePath+fileName+".mp4");
      muxer=new MediaMuxer(outputFile.getAbsolutePath(),MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

      MediaFormat format=MediaFormat.createVideoFormat("video/avc",w,h);
      format.setInteger(MediaFormat.KEY_COLOR_FORMAT,21);
      format.setInteger(MediaFormat.KEY_BIT_RATE,10000000);
      format.setInteger(MediaFormat.KEY_FRAME_RATE,fps);
      format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,1);

      codec=MediaCodec.createEncoderByType("video/avc");
      codec.configure(format,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
      codec.start();

      bufferInfo=new MediaCodec.BufferInfo();
      videoTrackIndex=muxer.addTrack(codec.getOutputFormat());

      audioTrackIndex=-1;
      if(hasAudio) {
        addAudioTrack();
      }

      muxer.start();
      muxerStarted=true;
      println("Muxer started");
    } catch(Exception e) {
      e.printStackTrace();
    }
  }

  void addAudioTrack() {
    try {
      MediaExtractor extractor=new MediaExtractor();
      extractor.setDataSource(audioPath);

      int numTracks=extractor.getTrackCount();
      println("Total tracks in audio file:"+numTracks);

      audioTrackIndex=-1;
      MediaFormat audioFormat=null;

      for(int i=0;i<numTracks;i++) {
        MediaFormat format=extractor.getTrackFormat(i);
        String mime=format.getString(MediaFormat.KEY_MIME);
        println("Track "+i+" MIME:"+mime);

        if(mime.startsWith("audio/")) {
          audioTrackIndex=i;
          audioFormat=format;
          break;
        }
      }

      if(audioTrackIndex==-1) {
        println("No valid audio track found!");
        extractor.release();
        return;
      }

      println("Audio track added:"+audioTrackIndex);
      int muxerAudioTrackIndex=muxer.addTrack(audioFormat);
      println("Muxer audio track index:"+muxerAudioTrackIndex);

      extractor.release();
    } catch(Exception e) {
      e.printStackTrace();
    }
  }

  void encodeFrame(PImage frame) {
    try {
      int inputBufferIndex=codec.dequeueInputBuffer(10000);
      if(inputBufferIndex>=0) {
        ByteBuffer inputBuffer=codec.getInputBuffer(inputBufferIndex);
        inputBuffer.clear();
        inputBuffer.put(getFrameData(frame));
        codec.queueInputBuffer(inputBufferIndex,0,inputBuffer.limit(),frameCount*1000000/fps,0);
      }

      int outputBufferIndex=codec.dequeueOutputBuffer(bufferInfo,10000);
      while(outputBufferIndex>=0) {
        ByteBuffer outputBuffer=codec.getOutputBuffer(outputBufferIndex);
        if(muxerStarted) {
          muxer.writeSampleData(videoTrackIndex,outputBuffer,bufferInfo);
        }
        codec.releaseOutputBuffer(outputBufferIndex,false);
        outputBufferIndex=codec.dequeueOutputBuffer(bufferInfo,10000);
      }
      frameCount++;
    } catch(Exception e) {
      e.printStackTrace();
    }
  }

  void addAudioData() {
    if(audioTrackIndex<0) {
      println("Audio track not added!");
      return;
    }

    audioExtractor=new MediaExtractor();
    try {
      audioExtractor.setDataSource(audioPath);
      println("Selecting audio track:"+audioTrackIndex);
      audioExtractor.selectTrack(audioTrackIndex);
      audioExtractor.seekTo(0,MediaExtractor.SEEK_TO_NEXT_SYNC);

      ByteBuffer buffer=ByteBuffer.allocate(1024*1024);
      MediaCodec.BufferInfo info=new MediaCodec.BufferInfo();
      while(true) {
        info.offset=0;
        info.size=audioExtractor.readSampleData(buffer,0);
        if(info.size<0) {
          println("End of audio stream");
          break;
        }

        info.presentationTimeUs=audioExtractor.getSampleTime();
        info.flags=audioExtractor.getSampleFlags();

        muxer.writeSampleData(audioTrackIndex,buffer,info);
        audioWriten=true;
        audioExtractor.advance();
      }
      audioExtractor.release();
    } catch(Exception e) {
      e.printStackTrace();
    }
  }

  public void stopRecording() {
    if(!recording) {
      println("Can't stop recording before it has started.");
      return;
    }

    try {
      codec.stop();
      codec.release();

      if(frameCount==0) {
        println("No video frames written! Muxer can't be stopped.");
        return;
      }

      addAudioData();

      if(audioTrackIndex>=0&&!audioWriten) {
        println("No audio data written! Muxer can't be stopped.");
        return;
      }

      if(muxerStarted) {
        muxer.stop();
        muxer.release();
        muxerStarted=false;
      }

      recording=false;
      println("Recording stopped successfully.");
    } catch(Exception e) {
      e.printStackTrace();
    }
  }

  ByteBuffer getFrameData(PImage frame) {  
    frame.loadPixels();  
    ByteBuffer buffer=ByteBuffer.allocate(w*h*3/2);  
    int yIndex=0,uvIndex=w*h;  

    for(int j=0;j<h;j++) {  
      for(int i=0;i<w;i++) {  
        int argb=frame.pixels[j*w+i];  
        int r=(argb>>16)&0xFF;  
        int g=(argb>>8)&0xFF;  
        int b=argb&0xFF;  

        int y=(int)(0.257*r+0.504*g+0.098*b+16);  
        int u=(int)(-0.148*r-0.291*g+0.439*b+128);  
        int v=(int)(0.439*r-0.368*g-0.071*b+128);  

        buffer.put(yIndex++,(byte)(y&0xFF));  
        if(j%2==0&&i%2==0) {  
          buffer.put(uvIndex++,(byte)(u&0xFF));  
          buffer.put(uvIndex++,(byte)(v&0xFF));  
        }  
      }  
    }  
    return buffer;  
  }  

  public boolean isRecording() {  
    return recording;  
  }  
}

I also wanted to add the ability to edit audio tracks: trim or merge two audio tracks for example. So far I have not been able to do anything.

I got different errors like “muxer is not initialized”, “failed to add the track to the muxer” or “failed to stop the muxer”.

I also want to say that English is not my native language and it is difficult for me to read reference books because they use too abstruse words.

Thank you in advance!

For clarification are you wanting to 1) add sound to your animation and then record both video and audio at the same time or 2) record your video animation and then add sound to it later? Using MediaPlayer I have code that will play sound simultaneously with your animation, but I have yet to be able to record both video and sound at the same time. What you are trying to do is not easy, and I’m not certain that I can do it either but am willing to try. The good news is that your last post didn’t crash and I will look at what you have added.

I’m not quite sure what you mean by that. Well, I guess I don’t care when to add audio, at the same time or after the video is recorded. The main thing is to make it convenient and work.

By the way, in encodeFrame() you can load an image from a folder or create and add PGraphics, like this: encodeFrame((PImage)pg). The main thing is to make sure the image is the right size. Same with the audio, I load it from a folder and I don’t need the audio to play along with the animation on the screen.

I also realized that you can only add audio in aac format, but that’s not very convenient, it would be nice to be able to add mp3 or wav audio, but that would probably require a decoder.

Wait, I just realized that in the future I probably want to edit audio and video frames directly in the program. If you know how you can play the video and audio frames synchronously on the screen, that would be very useful.

But that’s not the most important thing right now.

I managed to find code that converts wav to aac.

I’ll have to figure out how it works later. Right now I need to focus on how to add aac audio to video.

This source code plays an .mp3 audio file on an Android device. Make sure the file is inside a folder entitled ‘data’ inside your sketch folder. You would add this to your video code to get it to play alongside the animation.

import android.media.MediaPlayer;
import android.content.res.AssetFileDescriptor;
import android.content.Context;
import android.app.Activity;
import android.media.AudioAttributes;

MediaPlayer mp;
Context context; 
Activity act;
AssetFileDescriptor afd;

void setup(){
  size(720,720);

  act = this.getActivity();
  context = act.getApplicationContext();
  try {
    mp = new MediaPlayer();
    afd = context.getAssets().openFd("clip.mp3");//which is in the data folder
    println(afd);
    println("Successfully loaded audio file");
    mp.setAudioAttributes(
    new AudioAttributes.Builder()
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        .setUsage(AudioAttributes.USAGE_MEDIA)
        .build()
);
    mp.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
    mp.prepare();
    mp.start();
  }  catch(IOException e) {
    println("Media Player error:",e);
  }
}

void draw(){
  
}

I don’t know if you’re interested in using ffmpeg or not but it was used to add a sound track to an Android produced video using your code: https://www.dropbox.com/scl/fi/f10ijzm7vrs60thcmcvqr/video_merge.mp4.zip?rlkey=5zzdsko5wer9a35mq8278kpuh&st=yadll5iz&dl=0

Cmd-line code used:
ffmpeg

No ffmpeg is not a solution for me.

outputFile is never closed!

Here is a fix:

try {
    File outputFile = new File("/storage/emulated/0/Movies/muxer6.mp4");
    outStr = outputFile.getAbsolutePath();
    outputFile.close();
}
catch(Exception e) {
    outputFile.close();
    throw new RuntimeExeption("Could not prepare output file. Perhaps the output diorectory is not writable?"+e);
}

The following source code will allow you to merge a video file with an audio file, provided you use the correct formats: avc for the video and aac for the audio. I used ffmpeg to convert an .mp3 file to .m4a (mp4) but you could also strip the audio track from an mp4 with ffmpeg and use that. Both audio and video files need to be in a folder entitled ‘data’ inside of your sketch folder. The output is directed to your Android device. The reference that I used is: Mixing Audio Into Video on Android | sisik which is written in Kotlin but may be converted to java with an online converter.

Android code:

//Reference:
//https://sisik.eu/blog/android/media/mix-audio-into-video

import android.media.MediaExtractor;
import android.media.MediaMuxer;
import android.media.MediaFormat;
import android.media.MediaCodec;
import android.content.res.AssetFileDescriptor;
import android.content.Context;
import android.app.Activity;
import java.nio.ByteBuffer;
import java.io.*;

Muxer muxer;

String outStr = "";
String audioPath = "";
String videoPath = "";

Context context;
Activity act;
AssetFileDescriptor afd;
AssetFileDescriptor vfd;

void setup() {
  size(720, 720);
  act = this.getActivity();
  context = act.getApplicationContext();
  // Both video and audio are in the data folder
  // Audio needs to be in AAC format (mp4 or m4a)
  // a) Could use ffmpeg to strip audio track from mp4 video
  // or b) use ffmpeg to convert mp3 to m4a 
  // ffmpeg -i input.mp3 -c:a aac -b:a 192k output.m4a
  try { 
    vfd = context.getAssets().openFd("myVideo.mp4");
    afd = context.getAssets().openFd("myAudio.m4a");
  }
  catch(IOException e) {
    println("fileDescriptor exception: ", e);
  }
  videoPath = sketchPath("myVideo.mp4");
  audioPath = sketchPath("myAudio.m4a");
  println("audioPath: ", audioPath);
  println("videoPath: ", videoPath);

  try {
    File outputFile = new File("/storage/emulated/0/Movies/muxer6.mp4");
    outStr = outputFile.getAbsolutePath();
  }
  catch(Exception e) {
    println("outStr exception: ", e);
  }

  try {
    muxer = new Muxer(outStr);
  }
  catch(Exception e) {
    println("unable to initialize muxer:", e);
  }
}


Muxer class under separate Tab:

public class Muxer {
   public Muxer(String outFile) throws Exception { 
     
    // Init extractors which will get encoded frames
    MediaExtractor videoExtractor = new MediaExtractor();
    println("videoExtractor = ", videoExtractor);
    // **** This needs 3 parameters **** //
    videoExtractor.setDataSource(vfd.getFileDescriptor(), vfd.getStartOffset(), vfd.getLength());
    videoExtractor.selectTrack(0); // Assuming only one track per file. Adjust code if this is not the case.
    MediaFormat videoFormat = videoExtractor.getTrackFormat(0);

    MediaExtractor audioExtractor = new MediaExtractor();
    println("audioExtractor = ", audioExtractor);
    // **** This needs 3 parameters **** //
    audioExtractor.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength() );
    audioExtractor.selectTrack(0); // Assuming only one track per file. Adjust code if this is not the case.
    MediaFormat audioFormat = audioExtractor.getTrackFormat(0);
    // Init muxer
    MediaMuxer muxer = new MediaMuxer(outFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
    int videoIndex = muxer.addTrack(videoFormat);
    int audioIndex = muxer.addTrack(audioFormat);
    muxer.start();
    // Prepare buffer for copying
    int maxChunkSize = 1024 * 1024;
    ByteBuffer buffer = ByteBuffer.allocate(maxChunkSize);
    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
    // Copy Video
    while (true) {
      int chunkSize = videoExtractor.readSampleData(buffer, 0);
      if (chunkSize > 0) {
        bufferInfo.presentationTimeUs = videoExtractor.getSampleTime();
        bufferInfo.flags = videoExtractor.getSampleFlags();
        bufferInfo.size = chunkSize;
        muxer.writeSampleData(videoIndex, buffer, bufferInfo);
        videoExtractor.advance();
      } else {
        break;
      }
    }
    // Copy Audio
    while (true) {
      int chunkSize = audioExtractor.readSampleData(buffer, 0);
      if (chunkSize >= 0) {
        bufferInfo.presentationTimeUs = audioExtractor.getSampleTime();
        bufferInfo.flags = audioExtractor.getSampleFlags();
        bufferInfo.size = chunkSize;
        muxer.writeSampleData(audioIndex, buffer, bufferInfo);
        audioExtractor.advance();
      } else {
        break;
      }
    }
    // Cleanup
    muxer.stop();
    muxer.release();
    videoExtractor.release();
    audioExtractor.release();
  }
}

The catch blocks should end the program.

You should put a return statement at the end of each catch block.

That way, the program won’t try to process anything if the video file is non-existent, for example.

Don’t forget to close resources promptly, reliably, and effectively.

Make sure no resource is left unclosed.

Make sure the video export has its own dedicated Thread.

Declaring the variable every time is inefficient.

//inefficient
while (true) {
      int chunkSize = audioExtractor.readSampleData(buffer, 0);
      if (chunkSize >= 0) {
        bufferInfo.presentationTimeUs = audioExtractor.getSampleTime();
        bufferInfo.flags = audioExtractor.getSampleFlags();
        bufferInfo.size = chunkSize;
        muxer.writeSampleData(audioIndex, buffer, bufferInfo);
        audioExtractor.advance();
      } else {
        break;
      }
    }

This should be better:

//better
int chunkSize = audioExtractor.readSampleData(buffer, 0);
while (chunkSize >= 0) {
  bufferInfo.presentationTimeUs = audioExtractor.getSampleTime();
  bufferInfo.flags = audioExtractor.getSampleFlags();
  bufferInfo.size = chunkSize;
  muxer.writeSampleData(audioIndex, buffer, bufferInfo);
  audioExtractor.advance();
  chunkSize = audioExtractor.readSampleData(buffer, 0);
}

Why is Muxer public?
I suggest making it final and not public (just the default package-private)

println("outStr exception: ", e);
Should be: System.err.println("outStr exception: "+e)

Put a return in the catch block too.