8000 Merge pull request #46 from cloudwebrtc/media_recorder · flutter-webrtc/flutter-webrtc@1ef1c40 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1ef1c40

Browse files
authored
Merge pull request #46 from cloudwebrtc/media_recorder
Media recorder implementation on android
2 parents d1f8482 + 5c5545b commit 1ef1c40

12 files changed

+544
-7
lines changed

android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ android {
2525
compileSdkVersion 28
2626

2727
defaultConfig {
28-
minSdkVersion 17
28+
minSdkVersion 18
2929
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
3030
}
3131

android/src/main/java/com/cloudwebrtc/webrtc/FlutterWebRTCPlugin.java

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,22 +97,23 @@ private FlutterWebRTCPlugin(Registrar registrar, MethodChannel channel) {
9797
.setEnableInternalTracer(true)
9898
.createInitializationOptions());
9999

100+
// Initialize EGL contexts required for HW acceleration.
101+
EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
102+
103+
getUserMediaImpl = new GetUserMediaImpl(this, registrar.context());
104+
100105
final AudioDeviceModule audioDeviceModule = JavaAudioDeviceModule.builder(registrar.context())
101106
.setUseHardwareAcousticEchoCanceler(true)
102107
.setUseHardwareNoiseSuppressor(true)
108+
.setSamplesReadyCallback(getUserMediaImpl.audioSamplesInterceptor)
103109
.createAudioDeviceModule();
104110

105-
// Initialize EGL contexts required for HW acceleration.
106-
EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
107-
108111
mFactory = PeerConnectionFactory.builder()
109112
.setOptions(new PeerConnectionFactory.Options())
110113
.setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglContext, true, true))
111114
.setVideoDecoderFactory(new DefaultVideoDecoderFactory(eglContext))
112115
.setAudioDeviceModule(audioDeviceModule)
113116
.createPeerConnectionFactory();
114-
115-
getUserMediaImpl = new GetUserMediaImpl(this, registrar.context());
116117
}
117118

118119
@Override
@@ -278,7 +279,40 @@ public void onMethodCall(MethodCall call, Result result) {
278279
Map<String, Object> constraints = call.argument("constraints");
279280
ConstraintsMap constraintsMap = new ConstraintsMap(constraints);
280281
getDisplayMedia(constraintsMap, result);
281-
}else {
282+
}else if (call.method.equals("startRecordToFile")) {
283+
//This method can a lot of different exceptions
284+
//so we should notify plugin user about them
285+
try {
286+
String path = call.argument("path");
287+
VideoTrack videoTrack = null;
288+
String videoTrackId = call.argument("videoTrackId");
289+
if (videoTrackId != null) {
290+
MediaStreamTrack track = localTracks.get(videoTrackId);
291+
if (track instanceof VideoTrack)
292+
videoTrack = (VideoTrack) track;
293+
}
294+
AudioTrack audioTrack = null;
295+
String audioTrackId = call.argument("audioTrackId");
296+
Integer recorderId = call.argument("recorderId");
297+
if (audioTrackId != null) {
298+
MediaStreamTrack track = localTracks.get(audioTrackId);
299+
if (track instanceof AudioTrack)
300+
audioTrack = (AudioTrack) track;
301+
}
302+
if (videoTrack != null || audioTrack != null) {
303+
getUserMediaImpl.startRecordingToFile(path, recorderId, videoTrack, audioTrack);
304+
result.success(null);
305+
} else {
306+
result.error("0", "No track", null);
307+
}
308+
} catch (Exception e) {
309+
result.error("-1", e.getMessage(), e);
310+
}
311+
} else if (call.method.equals("stopRecordToFile")) {
312+
Integer recorderId = call.argument("recorderId");
313+
getUserMediaImpl.stopRecording(recorderId);
314+
result.success(null);
315+
} else {
282316
result.notImplemented();
283317
}
284318
}

android/src/main/java/com/cloudwebrtc/webrtc/GetUserMediaImpl.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,25 @@
1010
import android.os.Handler;
1111
import android.os.Looper;
1212
import android.os.ResultReceiver;
13+
import android.support.annotation.Nullable;
1314
import android.util.Log;
1415
import android.content.Intent;
1516
import android.app.Activity;
1617
import android.view.WindowManager;
1718
import android.media.projection.MediaProjection;
1819
import android.media.projection.MediaProjectionManager;
20+
import android.util.SparseArray;
1921

22+
import com.cloudwebrtc.webrtc.record.AudioSamplesInterceptor;
23+
import com.cloudwebrtc.webrtc.record.MediaRecorderImpl;
2024
import com.cloudwebrtc.webrtc.utils.Callback;
2125
import com.cloudwebrtc.webrtc.utils.ConstraintsArray;
2226
import com.cloudwebrtc.webrtc.utils.ConstraintsMap;
2327
import com.cloudwebrtc.webrtc.utils.EglUtils;
2428
import com.cloudwebrtc.webrtc.utils.ObjectType;
2529
import com.cloudwebrtc.webrtc.utils.PermissionUtils;
2630

31+
import java.io.File;
2732
import java.util.ArrayList;
2833
import java.util.HashMap;
2934
import java.util.List;
@@ -64,6 +69,9 @@ class GetUserMediaImpl{
6469
private MediaProjectionManager mProjectionManager = null;
6570
private static MediaProjection sMediaProjection = null;
6671

72+
final AudioSamplesInterceptor audioSamplesInterceptor = new AudioSamplesInterceptor();
73+
private final SparseArray<MediaRecorderImpl> mediaRecorders = new SparseArray<>();
74+
6775
public void screenRequestPremissions(ResultReceiver resultReceiver){
6876
Activity activity = plugin.getActivity();
6977

@@ -732,4 +740,27 @@ void switchCamera(String id) {
732740
cameraVideoCapturer.switchCamera(null);
733741
}
734742
}
743+
744+
/** Creates and starts recording of local stream to file
745+
* @param path to the file for record
746+
* @param videoTrack to record or null if only audio needed
747+
* @param audioTrack actually ignored, because current WebRTC implementation allows only
748+
* local track to be recorded, but if null passed, no audio will be recorded
749+
* @throws Exception lot of different exceptions, pass back to dart layer to print them at least
750+
* **/
751+
void startRecordingToFile(String path, Integer id, @Nullable VideoTrack videoTrack, @Nullable AudioTrack audioTrack) throws Exception {
752+
AudioSamplesInterceptor interceptor = audioTrack == null ? null : audioSamplesInterceptor;
753+
MediaRecorderImpl mediaRecorder = new MediaRecorderImpl(id, videoTrack, interceptor);
754+
mediaRecorder.startRecording(new File(path));
755+
mediaRecorders.append(id, mediaRecorder);
756+
}
757+
758+
void stopRecording(Integer id) {
759+
MediaRecorderImpl mediaRecorder = mediaRecorders.get(id);
760+
if (mediaRecorder != null) {
761+
mediaRecorder.stopRecording();
762+
mediaRecorders.remove(id);
763+
}
764+
}
765+
735766
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.cloudwebrtc.webrtc.record;
2+
3+
import android.annotation.SuppressLint;
4+
import android.util.Log;
5+
6+
import org.webrtc.audio.JavaAudioDeviceModule.SamplesReadyCallback;
7+
import org.webrtc.audio.JavaAudioDeviceModule.AudioSamples;
8+
9+
import java.util.HashMap;
10+
11+
/** JavaAudioDeviceModule allows attaching samples callback only on building
12+
* We don't want to instantiate VideoFileRenderer and codecs at this step
13+
* It's simple dummy class, it does nothing until samples are necessary */
14+
public class AudioSamplesInterceptor implements SamplesReadyCallback {
15+
16+
@SuppressLint("UseSparseArrays")
17+
private HashMap<Integer, SamplesReadyCallback> callbacks = new HashMap<>();
18+
19+
@Override
20+
public void onWebRtcAudioRecordSamplesReady(AudioSamples audioSamples) {
21+
for (SamplesReadyCallback callback : callbacks.values()) {
22+
callback.onWebRtcAudioRecordSamplesReady(audioSamples);
23+
}
24+
}
25+
26+
public void attachCallback(Integer id, SamplesReadyCallback callback) {
27+
callbacks.put(id, callback);
28+
}
29+
30+
public void detachCallback(Integer id) {
31+
callbacks.remove(id);
32+
}
33+
34+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.cloudwebrtc.webrtc.record;
2+
3+
import android.support.annotation.Nullable;
4+
import android.util.Log;
5+
6+
import com.cloudwebrtc.webrtc.utils.EglUtils;
7+
8+
import org.webrtc.VideoTrack;
9+
10+
import java.io.File;
11+
import java.io.IOException;
12+
13+
public class MediaRecorderImpl {
14+
15+
private final Integer id;
16+
private final VideoTrack videoTrack;
17+
private final AudioSamplesInterceptor audioInterceptor;
18+
private VideoFileRenderer videoFileRenderer;
19+
private boolean isRunning = false;
20+
21+
public MediaRecorderImpl(Integer id, @Nullable VideoTrack videoTrack, @Nullable AudioSamplesInterceptor audioInterceptor) {
22+
this.id = id;
23+
this.videoTrack = videoTrack;
24+
this.audioInterceptor = audioInterceptor;
25+
}
26+
27+
public void startRecording(File file) throws IOException {
28+
if (isRunning)
29+
return;
30+
isRunning = true;
31+
//noinspection ResultOfMethodCallIgnored
32+
file.getParentFile().mkdirs();
33+
if (videoTrack != null) {
34+
videoFileRenderer = new VideoFileRenderer(
35+
file.getAbsolutePath(),
36+
EglUtils.getRootEglBaseContext(),
37+
audioInterceptor != null
38+
);
39+
videoTrack.addSink(videoFileRenderer);
40+
if (audioInterceptor != null)
41+
audioInterceptor.attachCallback(id, videoFileRenderer);
42+
} else {
43+
Log.e(TAG, "Video track is null");
44+
if (audioInterceptor != null) {
45+
//TODO(rostopira): audio only recording
46+
}
47+
}
48+
}
49+
50+
public void stopRecording() {
51+
isRunning = false;
52+
if (audioInterceptor != null)
53+
audioInterceptor.detachCallback(id);
54+
if (videoTrack != null && videoFileRenderer != null) {
55+
videoTrack.removeSink(videoFileRenderer);
56+
videoFileRenderer.release();
57+
videoFileRenderer = null;
58+
}
59+
}
60+
61+
private static final String TAG = "MediaRecorderImpl";
62+
63+
}

0 commit comments

Comments
 (0)
0