10000 Merge branch 'master' of https://github.com/cloudwebrtc/flutter-webrtc · Condelab/flutter-webrtc@949fa1d · GitHub
[go: up one dir, main page]

Skip to content

Commit 949fa1d

Browse files
committed
2 parents d2e7bb1 + 93df393 commit 949fa1d

16 files changed

+675
-9
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Flutter-WebRTC
2-
[![pub package](https://img.shields.io/pub/v/flutter_webrtc.svg)](https://pub.dartlang.org/packages/flutter_webrtc)
2+
[![pub package](https://img.shields.io/pub/v/flutter_webrtc.svg)](https://pub.dartlang.org/packages/flutter_webrtc) [![Join the chat at https://gitter.im/flutter-webrtc/Lobby](https://badges.gitter.im/flutter-webrtc/Lobby.svg)](https://gitter.im/flutter-webrtc/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
3+
34
Flutter WebRTC plugin for iOS/Android
45

56
## Usage

android/build.gradle

Lines changed: 3 additions & 3 deletions
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

@@ -40,6 +40,6 @@ android {
40 8000 40
}
4141

4242
dependencies {
43-
api 'org.webrtc:google-webrtc:1.0.25821'
43+
api 'org.webrtc:google-webrtc:1.0.26131'
4444
implementation "com.android.support:support-annotations:22.0.0"
45-
}
45+
}

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

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
import android.util.Log;
88
import android.util.LongSparseArray;
99

10+
import com.cloudwebrtc.webrtc.record.FrameCapturer;
1011
import com.cloudwebrtc.webrtc.utils.ConstraintsArray;
1112
import com.cloudwebrtc.webrtc.utils.ConstraintsMap;
1213
import com.cloudwebrtc.webrtc.utils.EglUtils;
1314
import com.cloudwebrtc.webrtc.utils.ObjectType;
1415

16+
import java.io.File;
1517
import java.util.*;
1618

1719
import org.webrtc.AudioTrack;
@@ -97,22 +99,24 @@ private FlutterWebRTCPlugin(Registrar registrar, MethodChannel channel) {
9799
.setEnableInternalTracer(true)
98100
.createInitializationOptions());
99101

102+
// Initialize EGL contexts required for HW acceleration.
103+
EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
104+
105+
getUserMediaImpl = new GetUserMediaImpl(this, registrar.context());
106+
100107
final AudioDeviceModule audioDeviceModule = JavaAudioDeviceModule.builder(registrar.context())
101108
.setUseHardwareAcousticEchoCanceler(true)
102109
.setUseHardwareNoiseSuppressor(true)
110+
.setSamplesReadyCallback(getUserMediaImpl.audioSamplesInterceptor)
103111
.createAudioDeviceModule();
104112

105-
// Initialize EGL contexts required for HW acceleration.
106-
EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
107113

108114
mFactory = PeerConnectionFactory.builder()
109115
.setOptions(new PeerConnectionFactory.Options())
110116
.setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglContext, true, true))
111117
.setVideoDecoderFactory(new DefaultVideoDecoderFactory(eglContext))
112118
.setAudioDeviceModule(audioDeviceModule)
113119
.createPeerConnectionFactory();
114-
115-
getUserMediaImpl = new GetUserMediaImpl(this, registrar.context());
116120
}
117121

118122
@Override
@@ -278,7 +282,52 @@ public void onMethodCall(MethodCall call, Result result) {
278282
Map<String, Object> constraints = call.argument("constraints");
279283
ConstraintsMap constraintsMap = new ConstraintsMap(constraints);
280284
getDisplayMedia(constraintsMap, result);
281-
}else {
285+
}else if (call.method.equals("startRecordToFile")) {
286+
//This method can a lot of different exceptions
287+
//so we should notify plugin user about them
288+
try {
289+
String path = call.argument("path");
290+
VideoTrack videoTrack = null;
291+
String videoTrackId = call.argument("videoTrackId");
292+
if (videoTrackId != null) {
293+
MediaStreamTrack track = localTracks.get(videoTrackId);
294+
if (track instanceof VideoTrack)
295+
videoTrack = (VideoTrack) track;
296+
}
297+
AudioTrack audioTrack = null;
298+
String audioTrackId = call.argument("audioTrackId");
299+
Integer recorderId = call.argument("recorderId");
300+
if (audioTrackId != null) {
301+
MediaStreamTrack track = localTracks.get(audioTrackId);
302+
if (track instanceof AudioTrack)
303+
audioTrack = (AudioTrack) track;
304+
}
305+
if (videoTrack != null || audioTrack != null) {
306+
getUserMediaImpl.startRecordingToFile(path, recorderId, videoTrack, audioTrack);
307+
result.success(null);
308+
} else {
309+
result.error("0", "No track", null);
310+
}
311+
} catch (Exception e) {
312+
result.error("-1", e.getMessage(), e);
313+
}
314+
} else if (call.method.equals("stopRecordToFile")) {
315+
Integer recorderId = call.argument("recorderId");
316+
getUserMediaImpl.stopRecording(recorderId);
317+
result.success(null);
318+
} else if (call.method.equals("captureFrame")) {
319+
String path = call.argument("path");
320+
String videoTrackId = call.argument("trackId");
321+
if (videoTrackId != null) {
322+
MediaStreamTrack track = localTracks.get(videoTrackId);
323+
if (track instanceof VideoTrack)
324+
new FrameCapturer((VideoTrack) track, new File(path), result);
325+
else
326+
result.error("It's not video track", null, null);
327+
} else {
328+
result.error("Track is null", null, null);
329+
}
330+
} else {
282331
result.notImplemented();
283332
}
284333
}

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,34 @@
33
import android.Manifest;
44
import android.app.Fragment;
55
import android.app.FragmentTransaction;
6+
import android.content.ContentValues;
67
import android.content.Context;
78
import android.content.pm.PackageManager;
89
import android.os.Build;
910
import android.os.Bundle;
1011
import android.os.Handler;
1112
import android.os.Looper;
1213
import android.os.ResultReceiver;
14+
import android.provider.MediaStore;
15+
import android.support.annotation.Nullable;
1316
import android.util.Log;
1417
import android.content.Intent;
1518
import android.app.Activity;
1619
import android.view.WindowManager;
1720
import android.media.projection.MediaProjection;
1821
import android.media.projection.MediaProjectionManager;
22+
import android.util.SparseArray;
1923

24+
import com.cloudwebrtc.webrtc.record.AudioSamplesInterceptor;
25+
import com.cloudwebrtc.webrtc.record.MediaRecorderImpl;
2026
import com.cloudwebrtc.webrtc.utils.Callback;
2127
import com.cloudwebrtc.webrtc.utils.ConstraintsArray;
2228
import com.cloudwebrtc.webrtc.utils.ConstraintsMap;
2329
import com.cloudwebrtc.webrtc.utils.EglUtils;
2430
import com.cloudwebrtc.webrtc.utils.ObjectType;
2531
import com.cloudwebrtc.webrtc.utils.PermissionUtils;
2632

33+
import java.io.File;
2734
import java.util.ArrayList;
2835
import java.util.HashMap;
2936
import java.util.List;
@@ -64,6 +71,9 @@ class GetUserMediaImpl{
6471
private MediaProjectionManager mProjectionManager = null;
6572
private static MediaProjection sMediaProjection = null;
6673

74+
final AudioSamplesInterceptor audioSamplesInterceptor = new AudioSamplesInterceptor();
75+
private final SparseArray<MediaRecorderImpl> mediaRecorders = new SparseArray<>();
76+
6777
public void screenRequestPremissions(ResultReceiver resultReceiver){
6878
Activity activity = plugin.getActivity();
6979

@@ -732,4 +742,35 @@ void switchCamera(String id) {
732742
cameraVideoCapturer.switchCamera(null);
733743
}
734744
}
745+
746+
/** Creates and starts recording of local stream to file
747+
* @param path to the file for record
748+
* @param videoTrack to record or null if only audio needed
749+
* @param audioTrack actually ignored, because current WebRTC implementation allows only
750+
* local track to be recorded, but if null passed, no audio will be recorded
751+
* @throws Exception lot of different exceptions, pass back to dart layer to print them at least
752+
* **/
753+
void startRecordingToFile(String path, Integer id, @Nullable VideoTrack videoTrack, @Nullable AudioTrack audioTrack) throws Exception {
754+
AudioSamplesInterceptor interceptor = audioTrack == null ? null : audioSamplesInterceptor;
755+
MediaRecorderImpl mediaRecorder = new MediaRecorderImpl(id, videoTrack, interceptor);
756+
mediaRecorder.startRecording(new File(path));
757+
mediaRecorders.append(id, mediaRecorder);
758+
}
759+
760+
void stopRecording(Integer id) {
761+
MediaRecorderImpl mediaRecorder = mediaRecorders.get(id);
762+
if (mediaRecorder != null) {
763+
mediaRecorder.stopRecording();
764+
mediaRecorders.remove(id);
765+
File file = mediaRecorder.getRecordFile();
766+
if (file != null) {
767+
ContentValues values = new ContentValues(3);
768+
values.put(MediaStore.Video.Media.TITLE, file.getName());
769+
values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
770+
values.put(MediaStore.Video.Media.DATA, file.getAbsolutePath());
771+
applicationContext.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
772+
}
773+
}
774+
}
775+
735776
}
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: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.cloudwebrtc.webrtc.record;
2+
3+
import android.graphics.ImageFormat;
4+
import android.graphics.Rect;
5+
import android.graphics.YuvImage;
6+
import android.os.Handler;
7+
import android.os.Looper;
8+
9+
import org.webrtc.VideoFrame;
10+
import org.webrtc.VideoSink;
11+
import org.webrtc.VideoTrack;
12+
import org.webrtc.YuvHelper;
13+
14+
import java.io.File;
15+
import java.io.FileOutputStream;
16+
import java.io.IOException;
17+
import java.nio.ByteBuffer;
18+
19+
import io.flutter.plugin.common.MethodChannel;
20+
21+
public class FrameCapturer implements VideoSink {
22+
private VideoTrack videoTrack;
23+
private File file;
24+
private final MethodChannel.Result callback;
25+
private boolean gotFrame = false;
26+
27+
public FrameCapturer(VideoTrack track, File file, MethodChannel.Result callback) {
28+
videoTrack = track;
29+
this.file = file;
30+
this.callback = callback;
31+
track.addSink(this);
32+
}
33+
34+
@Override
35+
public void onFrame(VideoFrame videoFrame) {
36+
if (gotFrame)
37+
return;
38+
gotFrame = true;
39+
videoFrame.retain();
40+
VideoFrame.Buffer buffer = videoFrame.getBuffer();
41+
VideoFrame.I420Buffer i420Buffer = buffer.toI420();
42+
ByteBuffer y = i420Buffer.getDataY();
43+
ByteBuffer u = i420Buffer.getDataU();
44+
ByteBuffer v = i420Buffer.getDataV();
45+
int width = i420Buffer.getWidth();
46+
int height = i420Buffer.getHeight();
47+
int[] strides = new int[] {
48+
i420Buffer.getStrideY(),
49+
i420Buffer.getStrideU(),
50+
i420Buffer.getStrideV()
51+
};
52+
final int chromaWidth = (width + 1) / 2;
53+
final int chromaHeight = (height + 1) / 2;
54+
final int minSize = width * height + chromaWidth * chromaHeight * 2;
55+
ByteBuffer yuvBuffer = ByteBuffer.allocateDirect(minSize);
56+
YuvHelper.I420ToNV12(y, strides[0], v, strides[2], u, strides[1], yuvBuffer, width, height);
57+
YuvImage yuvImage = new YuvImage(
58+
yuvBuffer.array(),
59+
ImageFormat.NV21,
60+
width,
61+
height,
62+
strides
63+
);
64+
videoFrame.release();
65+
new Handler(Looper.getMainLooper()).post(() -> {
66+
videoTrack.removeSink(this);
67+
});
68+
try {
69+
if (!file.exists())
70+
//noinspection ResultOfMethodCallIgnored
71+
file.createNewFile();
72+
} catch (IOException io) {
73+
callback.error("IOException", io.getLocalizedMessage(), io);
74+
return;
75+
}
76+
try (FileOutputStream outputStream = new FileOutputStream(file)) {
77+
yuvImage.compressToJpeg(
78+
new Rect(0, 0, width, height),
79+
100,
80+
outputStream
81+
);
82+
callback.success(null);
83+
} catch (IOException io) {
84+
callback.error("IOException", io.getLocalizedMessage(), io);
85+
} catch (IllegalArgumentException iae) {
86+
callback.error("IllegalArgumentException", iae.getLocalizedMessage(), iae);
87+
} finally {
88+
file = null;
89+
}
90+
}
91+
92+
}

0 commit comments

Comments
 (0)
0