Trouble saving a video file with webrtc in Android
Asked Answered
R

1

4

I am developing a webrtc based video chat app, currently the video call is working, but I want to record a video from the remote video stream using the VideoFileRenderer, there are many implementations of the interface for example: https://chromium.googlesource.com/external/webrtc/+/master/sdk/android/api/org/webrtc/VideoFileRenderer.java this is the implementation I am using. It saves the video to the file with no problem but I can only play it with desktop after using a codec because the file is .y4m not .mp4 and when I try to play it using VideoView it says that it can't play the video, even if I try to play the video with the videoPlayer that comes with the android it can't play it, I can only play it using MXPlayer, VLC, or any other application that has codecs in desktop.

to simplify the question: How can I play video.y4m on native android VideoView?

I will simplify it more, I will assume that I don't understand the format of the recorded file, here is the code I am using to record the file:

When start recording:

remoteVideoFileRenderer = new VideoFileRenderer(
                fileToRecordTo.getAbsolutePath(),
                640,
                480,
                rootEglBase.getEglBaseContext());
        remoteVideoTrack.addSink(remoteVideoFileRenderer);

When finish recording:

remoteVideoFileRenderer.release();

Now the question again: I have a "fileToRecordTo" and this video file can be played on GOM(windows), VLC(windows, mac and Android), MXPlayer(Android) but I can't neither play it using the player that comes embedded with the Android(if worked, I would have used this player in my app) nor on Android native videoView.

any help.

Rote answered 16/3, 2019 at 21:43 Comment(7)
Having the same problem, have you thought about using maybe "MediaMuxer" to converct the raw frames and then save it to mp4 instead of y4m?Pagano
I thought about that, then I found out that the file has only video, but no audio, I did not figure out how I will get the audio file, then I will try to do that, then if worked, I will add the answer, please help me if u know how to do that @AviramFirebergerRote
Haven't done it, But if you manage to do so with video only please share how you manage to do it and then I might be able to helpPagano
Don't worry @AviramFireberger, surly I will and will tag u here once its done, the thing is I am a little bit busy these days, but I promise i will continue working on this, then will tag u when done, I will even try to contact u directly via ur emailRote
Were you able to record audio during the RTC call?Mcclimans
@ArmaniDarkKnight Did you have any luck recording the audio(along with video)?Boysenberry
no, unfortunately not, please if you got luck making it please answer the question and I will accept the answer and give you vote up, thanks in advanceRote
B
8

Video only recording

I had a similar case in my project. At first, I tried WebRTC's default VideoFileRenderer but the video size was too big because no compression is applied. I found this repository. It really helped in my case. https://github.com/cloudwebrtc/flutter-webrtc

Here is a step by step guide. I've also made some adjustments.

Add this class to your project. It has lots of options to configure the final video format.

import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.view.Surface;

import org.webrtc.EglBase;
import org.webrtc.GlRectDrawer;
import org.webrtc.VideoFrame;
import org.webrtc.VideoFrameDrawer;
import org.webrtc.VideoSink;

import java.io.IOException;
import java.nio.ByteBuffer;

class FileEncoder implements VideoSink {
    private static final String TAG = "FileRenderer";
    private final HandlerThread renderThread;
    private final Handler renderThreadHandler;
    private int outputFileWidth = -1;
    private int outputFileHeight = -1;
    private ByteBuffer[] encoderOutputBuffers;
    private EglBase eglBase;
    private EglBase.Context sharedContext;
    private VideoFrameDrawer frameDrawer;
    private static final String MIME_TYPE = "video/avc";    // H.264 Advanced Video Coding
    private static final int FRAME_RATE = 30;               // 30fps
    private static final int IFRAME_INTERVAL = 5;           // 5 seconds between I-frames
    private MediaMuxer mediaMuxer;
    private MediaCodec encoder;
    private MediaCodec.BufferInfo bufferInfo;
    private int trackIndex = -1;
    private boolean isRunning = true;
    private GlRectDrawer drawer;
    private Surface surface;

    FileEncoder(String outputFile, final EglBase.Context sharedContext) throws IOException {
        renderThread = new HandlerThread(TAG + "RenderThread");
        renderThread.start();
        renderThreadHandler = new Handler(renderThread.getLooper());
        bufferInfo = new MediaCodec.BufferInfo();
        this.sharedContext = sharedContext;

        mediaMuxer = new MediaMuxer(outputFile,
                MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
    }

    private void initVideoEncoder() {
        MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, 1280, 720);

        format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        format.setInteger(MediaFormat.KEY_BIT_RATE, 6000000);
        format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);

        try {
            encoder = MediaCodec.createEncoderByType(MIME_TYPE);
            encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            renderThreadHandler.post(() -> {
                eglBase = EglBase.create(sharedContext, EglBase.CONFIG_RECORDABLE);
                surface = encoder.createInputSurface();
                eglBase.createSurface(surface);
                eglBase.makeCurrent();
                drawer = new GlRectDrawer();
            });
        } catch (Exception e) {
            Log.wtf(TAG, e);
        }
    }

    @Override
    public void onFrame(VideoFrame frame) {
        frame.retain();
        if (outputFileWidth == -1) {
            outputFileWidth = frame.getRotatedWidth();
            outputFileHeight = frame.getRotatedHeight();
            initVideoEncoder();
        }
        renderThreadHandler.post(() -> renderFrameOnRenderThread(frame));
    }

    private void renderFrameOnRenderThread(VideoFrame frame) {
        if (frameDrawer == null) {
            frameDrawer = new VideoFrameDrawer();
        }
        frameDrawer.drawFrame(frame, drawer, null, 0, 0, outputFileWidth, outputFileHeight);
        frame.release();
        drainEncoder();
        eglBase.swapBuffers();
    }

    /**
     * Release all resources. All already posted frames will be rendered first.
     */
    void release() {
        isRunning = false;
        renderThreadHandler.post(() -> {
            if (encoder != null) {
                encoder.stop();
                encoder.release();
            }
            eglBase.release();
            mediaMuxer.stop();
            mediaMuxer.release();
            renderThread.quit();
        });
    }

    private boolean encoderStarted = false;
    private volatile boolean muxerStarted = false;
    private long videoFrameStart = 0;

    private void drainEncoder() {
        if (!encoderStarted) {
            encoder.start();
            encoderOutputBuffers = encoder.getOutputBuffers();
            encoderStarted = true;
            return;
        }
        while (true) {
            int encoderStatus = encoder.dequeueOutputBuffer(bufferInfo, 10000);
            if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
                break;
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                // not expected for an encoder
                encoderOutputBuffers = encoder.getOutputBuffers();
                Log.e(TAG, "encoder output buffers changed");
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // not expected for an encoder
                MediaFormat newFormat = encoder.getOutputFormat();

                Log.e(TAG, "encoder output format changed: " + newFormat);
                trackIndex = mediaMuxer.addTrack(newFormat);
                if (!muxerStarted) {
                    mediaMuxer.start();
                    muxerStarted = true;
                }
                if (!muxerStarted)
                    break;
            } else if (encoderStatus < 0) {
                Log.e(TAG, "unexpected result fr om encoder.dequeueOutputBuffer: " + encoderStatus);
            } else { // encoderStatus >= 0
                try {
                    ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
                    if (encodedData == null) {
                        Log.e(TAG, "encoderOutputBuffer " + encoderStatus + " was null");
                        break;
                    }
                    // It's usually necessary to adjust the ByteBuffer values to match BufferInfo.
                    encodedData.position(bufferInfo.offset);
                    encodedData.limit(bufferInfo.offset + bufferInfo.size);
                    if (videoFrameStart == 0 && bufferInfo.presentationTimeUs != 0) {
                        videoFrameStart = bufferInfo.presentationTimeUs;
                    }
                    bufferInfo.presentationTimeUs -= videoFrameStart;
                    if (muxerStarted)
                        mediaMuxer.writeSampleData(trackIndex, encodedData, bufferInfo);
                    isRunning = isRunning && (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == 0;
                    encoder.releaseOutputBuffer(encoderStatus, false);
                    if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                        break;
                    }
                } catch (Exception e) {
                    Log.wtf(TAG, e);
                    break;
                }
            }
        }
    }

    private long presTime = 0L;

}

Now on your Activity/Fragment class

Declare a variable of the above class

FileEncoder recording;

When you receive the stream you want to record(remote or local) you can initialize the recording.

FileEncoder recording = new FileEncoder("path/to/video", rootEglBase.eglBaseContext)
remoteVideoTrack.addSink(recording)

When the call session ends, you need to stop and release the recording.

remoteVideoTrack.removeSink(recording)
recording.release()

This is enough to record the video but without audio.

Video & Audio recording

To record local peer's audio you need to consume this class(https://webrtc.googlesource.com/src/+/master/examples/androidapp/src/org/appspot/apprtc/RecordedAudioToFileController.java). But first you need to setup an AudioDeviceModule object

AudioDeviceModule adm = createJavaAudioDevice()
peerConnectionFactory = PeerConnectionFactory.builder()
    .setOptions(options)
    .setAudioDeviceModule(adm)
    .setVideoEncoderFactory(defaultVideoEncoderFactory)
    .setVideoDecoderFactory(defaultVideoDecoderFactory)
    .createPeerConnectionFactory()
adm.release()

private AudioDeviceModule createJavaAudioDevice() {
    //Implement AudioRecordErrorCallback
    //Implement AudioTrackErrorCallback
 return JavaAudioDeviceModule.builder(this)
    .setSamplesReadyCallback(audioRecorder)
    //Default audio source is Voice Communication which is good for VoIP sessions. You can change to the audio source you want.
    .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
    .setAudioRecordErrorCallback(audioRecordErrorCallback)
    .setAudioTrackErrorCallback(audioTrackErrorCallback)
    .createAudioDeviceModule()

}

Merge audio and video

Add this dependency

implementation 'com.googlecode.mp4parser:isoparser:1.1.22'

Then add this piece to your code when your call finishes. Make sure that video and audio recording are stopped and released properly.

try {
     Movie video;
     video = MovieCreator.build("path/to/recorded/video");
     Movie audio;
     audio = MovieCreator.build("path/to/recorded/audio");
     Track audioTrack = audio.getTracks().get(0)  
     video.addTrack(audioTrack);
     Container out = new DefaultMp4Builder().build(video);
     FileChannel fc = new FileOutputStream(new File("path/to/final/output")).getChannel();
     out.writeContainer(fc);
     fc.close();
} catch (IOException e) {
     e.printStackTrace();
}

I know this isn't the best solution for recording audio and video in an Android WebRTC video call. If someone knows how to extract audio using WebRTC please add a comment.

Boysenberry answered 2/11, 2019 at 11:19 Comment(5)
You really did great job mate.Rote
@Oliver Where do you get remoteVideoTrack from?Jermainejerman
@Oliver Where i have to add FileEncoder recording = new FileEncoder("path/to/video", rootEglBase.eglBaseContext) remoteVideoTrack.addSink(recording) in CallActivity class.Sedlik
Hey @Oliver wondering if the audio collected here is from the other person's side (via webrtc), or from the mic input?Schutz
Here in fileencoder file null value is also writter after that frame encoded data is written.Steviestevy

© 2022 - 2024 — McMap. All rights reserved.