1
1
package com .cloudwebrtc .webrtc .record ;
2
2
3
- import android .media .AudioFormat ;
4
3
import android .media .MediaCodec ;
5
4
import android .media .MediaCodecInfo ;
6
5
import android .media .MediaFormat ;
@@ -34,12 +33,14 @@ class VideoFileRenderer implements VideoSink, SamplesReadyCallback {
34
33
private final Handler audioThreadHandler ;
35
34
private final FileOutputStream videoOutFile ;
36
35
private final String outputFileName ;
37
- private final int outputFileWidth ;
38
- private final int outputFileHeight ;
36
+ private int outputFileWidth = - 1 ;
37
+ private int outputFileHeight = - 1 ;
39
38
private ByteBuffer [] encoderOutputBuffers ;
40
39
private ByteBuffer [] audioInputBuffers ;
41
40
private ByteBuffer [] audioOutputBuffers ;
42
41
private EglBase eglBase ;
42
+ private EglBase .Context sharedContext ;
43
+ private VideoFrameDrawer frameDrawer ;
43
44
44
45
// TODO: these ought to be configurable as well
45
46
private static final String MIME_TYPE = "video/avc" ; // H.264 Advanced Video Coding
@@ -49,23 +50,18 @@ class VideoFileRenderer implements VideoSink, SamplesReadyCallback {
49
50
private MediaMuxer mediaMuxer ;
50
51
private MediaCodec encoder ;
51
52
private MediaCodec .BufferInfo bufferInfo ;
52
- private int trackIndex ;
53
- private int audioTrackIndex = - 1 ;
53
+ private int trackIndex = - 1 ;
54
+ private int audioTrackIndex ;
54
55
private boolean isRunning = true ;
55
56
private GlRectDrawer drawer ;
56
57
private Surface surface ;
57
58
private MediaCodec audioEncoder ;
58
- private long framesCount = 0 ;
59
- private long samplesCount = 0 ;
60
59
61
- VideoFileRenderer (String outputFile , int outputFileWidth , int outputFileHeight ,
62
- final EglBase .Context sharedContext ) throws IOException {
60
+ VideoFileRenderer (String outputFile , final EglBase .Context sharedContext , boolean withAudio ) throws IOException {
63
61
if ((outputFileWidth % 2 ) == 1 || (outputFileHeight % 2 ) == 1 ) {
64
62
throw new IllegalArgumentException ("Does not support uneven width or height" );
65
63
}
66
64
this .outputFileName = outputFile ;
67
- this .outputFileWidth = outputFileWidth ;
68
- this .outputFileHeight = outputFileHeight ;
69
65
videoOutFile = new FileOutputStream (outputFile );
70
66
renderThread = new HandlerThread (TAG + "RenderThread" );
71
67
renderThread .start ();
@@ -77,7 +73,18 @@ class VideoFileRenderer implements VideoSink, SamplesReadyCallback {
77
73
audioThreadHandler = new Handler (audioThread .getLooper ());
78
74
fileThreadHandler = new Handler (fileThread .getLooper ());
79
75
bufferInfo = new MediaCodec .BufferInfo ();
76
+ this .sharedContext = sharedContext ;
77
+
78
+ // Create a MediaMuxer. We can't add the video track and start() the muxer here,
79
+ // because our MediaFormat doesn't have the Magic Goodies. These can only be
80
+
57AE
// obtained from the encoder after it has started processing data.
81
+ mediaMuxer = new MediaMuxer (outputFile ,
82
+ MediaMuxer .OutputFormat .MUXER_OUTPUT_MPEG_4 );
80
83
84
+ audioTrackIndex = withAudio ? 0 : -1 ;
85
+ }
86
+
87
+ private void initVideoEncoder () {
81
88
MediaFormat format = MediaFormat .createVideoFormat (MIME_TYPE , outputFileWidth , outputFileHeight );
82
89
83
90
// Set some properties. Failing to specify some of these can cause the MediaCodec
@@ -90,71 +97,59 @@ class VideoFileRenderer implements VideoSink, SamplesReadyCallback {
90
97
91
98
// Create a MediaCodec encoder, and configure it with our format. Get a Surface
92
99
// we can use for input and wrap it with a class that handles the EGL work.
93
- encoder = MediaCodec .createEncoderByType (MIME_TYPE );
94
- encoder .configure (format , null , null , MediaCodec .CONFIGURE_FLAG_ENCODE );
95
- ThreadUtils .invokeAtFrontUninterruptibly (renderThreadHandler , new Runnable () {
96
- @ Override
97
- public void run () {
100
+ try {
101
+ encoder = MediaCodec .createEncoderByType (MIME_TYPE );
102
+ encoder .configure (format , null , null , MediaCodec .CONFIGURE_FLAG_ENCODE );
103
+ renderThreadHandler .post (() -> {
98
104
eglBase = EglBase .create (sharedContext , EglBase .CONFIG_RECORDABLE );
99
105
surface = encoder .createInputSurface ();
100
106
eglBase .createSurface (surface );
101
107
eglBase .makeCurrent ();
102
108
drawer = new GlRectDrawer ();
103
- }
104
- });
105
-
106
- // Create a MediaMuxer. We can't add the video track and start() the muxer here,
107
- // because our MediaFormat doesn't have the Magic Goodies. These can only be
108
- // obtained from the encoder after it has started processing data.
109
- //
110
- // We're not actually interested in multiplexing audio. We just want to convert
111
- // the raw H.264 elementary stream we get from MediaCodec into a .mp4 file.
112
- mediaMuxer = new MediaMuxer (outputFile ,
113
- MediaMuxer .OutputFormat .MUXER_OUTPUT_MPEG_4 );
114
-
115
- trackIndex = -1 ;
109
+ });
110
+ } catch (Exception e ) {
111
+ Log .wtf (TAG , e );
112
+ }
116
113
}
117
114
118
115
@ Override
119
116
public void onFrame (VideoFrame frame ) {
120
117
frame .retain ();
118
+ if (outputFileWidth == -1 ) {
119
+ outputFileWidth = frame .getRotatedWidth ();
120
+ outputFileHeight = frame .getRotatedHeight ();
121
+ initVideoEncoder ();
122
+ }
121
123
renderThreadHandler .post (() -> renderFrameOnRenderThread (frame ));
122
124
}
123
125
124
- private VideoFrameDrawer frameDrawer ;
125
-
126
126
private void renderFrameOnRenderThread (VideoFrame frame ) {
127
127
if (frameDrawer == null ) {
128
128
frameDrawer = new VideoFrameDrawer ();
129
129
}
130
- framesCount ++;
131
130
frameDrawer .drawFrame (frame , drawer , null , 0 , 0 , outputFileWidth , outputFileHeight );
132
131
frame .release ();
133
- drainEncoder (false );
132
+ drainEncoder ();
134
133
eglBase .swapBuffers ();
135
134
}
136
135
137
136
/**
138
137
* Release all resources. All already posted frames will be rendered first.
139
138
*/
140
139
void release () {
141
- Log .e (TAG , "Frames " + String .valueOf (framesCount ) + " samples " + String .valueOf (samplesCount ));
142
140
final CountDownLatch cleanupBarrier = new CountDownLatch (2 );
143
141
isRunning = false ;
144
142
renderThreadHandler .post (() -> {
145
- // encoder.flush();
146
- //drainEncoder(false);
147
143
encoder .stop ();
148
144
encoder .release ();
149
145
eglBase .release ();
150
146
renderThread .quit ();
151
147
cleanupBarrier .countDown ();
152
148
});
153
149
audioThreadHandler .post (() -> {
154
- // audioEncoder.flush();
155
- //drainAudio();
156
150
audioEncoder .stop ();
157
151
audioEncoder .release ();
152
+ audioThread .quit ();
158
153
cleanupBarrier .countDown ();
159
154
});
160
155
ThreadUtils .awaitUninterruptibly (cleanupBarrier );
@@ -181,7 +176,7 @@ void release() {
181
176
private volatile boolean muxerStarted = false ;
182
177
private long videoFrameStart = 0 ;
183
178
184
- private void drainEncoder (boolean endOfStream ) {
179
+ private void drainEncoder () {
185
180
if (!encoderStarted ) {
186
181
encoder .start ();
187
182
encoderOutputBuffers = encoder .getOutputBuffers ();
@@ -191,9 +186,7 @@ private void drainEncoder(boolean endOfStream) {
191
186
while (true ) {
192
187
int encoderStatus = encoder .dequeueOutputBuffer (bufferInfo , 10000 );
193
188
if (encoderStatus == MediaCodec .INFO_TRY_AGAIN_LATER ) {
194
- Log .i (TAG , "no output from encoder available" );
195
- if (!endOfStream )
196
- break ;
189
+ break ;
197
190
} else if (encoderStatus == MediaCodec .INFO_OUTPUT_BUFFERS_CHANGED ) {
198
191
// not expected for an encoder
199
192
encoderOutputBuffers = encoder .getOutputBuffers ();
@@ -217,6 +210,7 @@ private void drainEncoder(boolean endOfStream) {
217
210
ByteBuffer encodedData = encoderOutputBuffers [encoderStatus ];
218
211
if (encodedData == null ) {
219
212
Log .e (TAG , "encoderOutputBuffer " + encoderStatus + " was null" );
213
+ break ;
220
214
}
221
215
// It's usually necessary to adjust the ByteBuffer values to match BufferInfo.
222
216
encodedData .position (bufferInfo .offset );
@@ -248,17 +242,16 @@ private void drainAudio() {
248
242
while (true ) {
249
243
int encoderStatus = audioEncoder .dequeueOutputBuffer (bufferInfo , 10000 );
250
244
if (encoderStatus == MediaCodec .INFO_TRY_AGAIN_LATER ) {
251
- Log .i (TAG , "no output from audio encoder available" );
252
245
break ;
253
246
} else if (encoderStatus == MediaCodec .INFO_OUTPUT_BUFFERS_CHANGED ) {
254
247
// not expected for an encoder
255
248
audioOutputBuffers = audioEncoder .getOutputBuffers ();
256
- Log .e (TAG , "encoder output buffers changed" );
249
+ Log .w (TAG , "encoder output buffers changed" );
257
250
} else if (encoderStatus == MediaCodec .INFO_OUTPUT_FORMAT_CHANGED ) {
258
251
// not expected for an encoder
259
252
MediaFormat newFormat = audioEncoder .getOutputFormat ();
260
253
261
- Log .e (TAG , "encoder output format changed: " + newFormat );
254
+ Log .w (TAG , "encoder output format changed: " + newFormat );
262
255
audioTrackIndex = mediaMuxer .addTrack (newFormat );
263
256
if (trackIndex != -1 && !muxerStarted ) {
264
257
mediaMuxer .start ();
@@ -299,7 +292,6 @@ private void drainAudio() {
299
292
public void onWebRtcAudioRecordSamplesReady (JavaAudioDeviceModule .AudioSamples audioSamples ) {
300
293
if (!isRunning )
301
294
return ;
302
- samplesCount ++;
303
295
audioThreadHandler .post (() -> {
304
296
if (audioEncoder == null ) try {
305
297
audioEncoder = MediaCodec .createEncoderByType ("audio/mp4a-latm" );
0 commit comments