Skip to content

Commit 68e6004

Browse files
committed
Frame capture feature
1 parent 2ef515c commit 68e6004

File tree

3 files changed

+113
-0
lines changed

3 files changed

+113
-0
lines changed

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

Lines changed: 15 additions & 0 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;
@@ -108,6 +110,7 @@ private FlutterWebRTCPlugin(Registrar registrar, MethodChannel channel) {
108110
.setSamplesReadyCallback(getUserMediaImpl.audioSamplesInterceptor)
109111
.createAudioDeviceModule();
110112

113+
111114
mFactory = PeerConnectionFactory.builder()
112115
.setOptions(new PeerConnectionFactory.Options())
113116
.setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglContext, true, true))
@@ -312,6 +315,18 @@ public void onMethodCall(MethodCall call, Result result) {
312315
Integer recorderId = call.argument("recorderId");
313316
getUserMediaImpl.stopRecording(recorderId);
314317
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+
}
315330
} else {
316331
result.notImplemented();
317332
}
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+
}

lib/media_stream_track.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ class MediaStreamTrack {
3535
);
3636
}
3737

38+
captureFrame(String filePath) =>
39+
_channel.invokeMethod(
40+
'captureFrame',
41+
<String, dynamic>{'trackId':_trackId, 'path': filePath},
42+
);
43+
3844
Future<void> dispose() async {
3945
await _channel.invokeMethod(
4046
'trackDispose',

0 commit comments

Comments
 (0)