Extract video streaming to a separate class
ScreenEncoder handled both capture/encoding and sending over the network. Move the streaming part to a separate VideoStreamer.
This commit is contained in:
parent
3aac74e9e9
commit
87972e2022
3 changed files with 67 additions and 40 deletions
|
@ -12,7 +12,6 @@ import android.os.IBinder;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
|
|
||||||
import java.io.FileDescriptor;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -22,6 +21,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public class ScreenEncoder implements Device.RotationListener {
|
public class ScreenEncoder implements Device.RotationListener {
|
||||||
|
|
||||||
|
public interface Callbacks {
|
||||||
|
void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException;
|
||||||
|
}
|
||||||
|
|
||||||
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
|
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
|
||||||
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
|
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
|
||||||
private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder";
|
private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder";
|
||||||
|
@ -30,25 +33,18 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||||
private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800};
|
private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800};
|
||||||
private static final int MAX_CONSECUTIVE_ERRORS = 3;
|
private static final int MAX_CONSECUTIVE_ERRORS = 3;
|
||||||
|
|
||||||
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
|
||||||
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
|
||||||
|
|
||||||
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
||||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
|
||||||
|
|
||||||
private final String encoderName;
|
private final String encoderName;
|
||||||
private final List<CodecOption> codecOptions;
|
private final List<CodecOption> codecOptions;
|
||||||
private final int bitRate;
|
private final int bitRate;
|
||||||
private final int maxFps;
|
private final int maxFps;
|
||||||
private final boolean sendFrameMeta;
|
|
||||||
private final boolean downsizeOnError;
|
private final boolean downsizeOnError;
|
||||||
|
|
||||||
private boolean firstFrameSent;
|
private boolean firstFrameSent;
|
||||||
private int consecutiveErrors;
|
private int consecutiveErrors;
|
||||||
|
|
||||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
|
public ScreenEncoder(int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName, boolean downsizeOnError) {
|
||||||
boolean downsizeOnError) {
|
|
||||||
this.sendFrameMeta = sendFrameMeta;
|
|
||||||
this.bitRate = bitRate;
|
this.bitRate = bitRate;
|
||||||
this.maxFps = maxFps;
|
this.maxFps = maxFps;
|
||||||
this.codecOptions = codecOptions;
|
this.codecOptions = codecOptions;
|
||||||
|
@ -65,7 +61,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||||
return rotationChanged.getAndSet(false);
|
return rotationChanged.getAndSet(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
public void streamScreen(Device device, Callbacks callbacks) throws IOException {
|
||||||
MediaCodec codec = createCodec(encoderName);
|
MediaCodec codec = createCodec(encoderName);
|
||||||
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
|
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
|
||||||
IBinder display = createDisplay();
|
IBinder display = createDisplay();
|
||||||
|
@ -94,7 +90,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||||
|
|
||||||
codec.start();
|
codec.start();
|
||||||
|
|
||||||
alive = encode(codec, fd);
|
alive = encode(codec, callbacks);
|
||||||
// do not call stop() on exception, it would trigger an IllegalStateException
|
// do not call stop() on exception, it would trigger an IllegalStateException
|
||||||
codec.stop();
|
codec.stop();
|
||||||
} catch (IllegalStateException | IllegalArgumentException e) {
|
} catch (IllegalStateException | IllegalArgumentException e) {
|
||||||
|
@ -163,7 +159,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
|
private boolean encode(MediaCodec codec, Callbacks callbacks) throws IOException {
|
||||||
boolean eof = false;
|
boolean eof = false;
|
||||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||||
|
|
||||||
|
@ -179,16 +175,14 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||||
if (outputBufferId >= 0) {
|
if (outputBufferId >= 0) {
|
||||||
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
|
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
|
||||||
|
|
||||||
if (sendFrameMeta) {
|
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
|
||||||
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
|
if (!isConfig) {
|
||||||
}
|
|
||||||
|
|
||||||
IO.writeFully(fd, codecBuffer);
|
|
||||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
|
|
||||||
// If this is not a config packet, then it contains a frame
|
// If this is not a config packet, then it contains a frame
|
||||||
firstFrameSent = true;
|
firstFrameSent = true;
|
||||||
consecutiveErrors = 0;
|
consecutiveErrors = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
callbacks.onPacket(codecBuffer, bufferInfo);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (outputBufferId >= 0) {
|
if (outputBufferId >= 0) {
|
||||||
|
@ -200,25 +194,6 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||||
return !eof;
|
return !eof;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
|
|
||||||
headerBuffer.clear();
|
|
||||||
|
|
||||||
long pts;
|
|
||||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
|
||||||
pts = PACKET_FLAG_CONFIG; // non-media data packet
|
|
||||||
} else {
|
|
||||||
pts = bufferInfo.presentationTimeUs;
|
|
||||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
|
|
||||||
pts |= PACKET_FLAG_KEY_FRAME;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
headerBuffer.putLong(pts);
|
|
||||||
headerBuffer.putInt(packetSize);
|
|
||||||
headerBuffer.flip();
|
|
||||||
IO.writeFully(fd, headerBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MediaCodecInfo[] listEncoders() {
|
private static MediaCodecInfo[] listEncoders() {
|
||||||
List<MediaCodecInfo> result = new ArrayList<>();
|
List<MediaCodecInfo> result = new ArrayList<>();
|
||||||
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||||
|
|
|
@ -89,8 +89,8 @@ public final class Server {
|
||||||
Size videoSize = device.getScreenInfo().getVideoSize();
|
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||||
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
||||||
}
|
}
|
||||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions,
|
ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate(), options.getMaxFps(), codecOptions, options.getEncoderName(),
|
||||||
options.getEncoderName(), options.getDownsizeOnError());
|
options.getDownsizeOnError());
|
||||||
|
|
||||||
Controller controller = null;
|
Controller controller = null;
|
||||||
if (control) {
|
if (control) {
|
||||||
|
@ -103,7 +103,8 @@ public final class Server {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// synchronous
|
// synchronous
|
||||||
screenEncoder.streamScreen(device, connection.getVideoFd());
|
VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), options.getSendFrameMeta());
|
||||||
|
screenEncoder.streamScreen(device, videoStreamer);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// this is expected on close
|
// this is expected on close
|
||||||
Ln.d("Screen streaming stopped");
|
Ln.d("Screen streaming stopped");
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import android.media.MediaCodec;
|
||||||
|
|
||||||
|
import java.io.FileDescriptor;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public final class VideoStreamer implements ScreenEncoder.Callbacks {
|
||||||
|
|
||||||
|
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
||||||
|
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
||||||
|
|
||||||
|
private final FileDescriptor fd;
|
||||||
|
private final boolean sendFrameMeta;
|
||||||
|
|
||||||
|
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||||
|
|
||||||
|
public VideoStreamer(FileDescriptor fd, boolean sendFrameMeta) {
|
||||||
|
this.fd = fd;
|
||||||
|
this.sendFrameMeta = sendFrameMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
|
||||||
|
if (sendFrameMeta) {
|
||||||
|
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
|
||||||
|
}
|
||||||
|
|
||||||
|
IO.writeFully(fd, codecBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
|
||||||
|
headerBuffer.clear();
|
||||||
|
|
||||||
|
long ptsAndFlags;
|
||||||
|
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
||||||
|
ptsAndFlags = PACKET_FLAG_CONFIG; // non-media data packet
|
||||||
|
} else {
|
||||||
|
ptsAndFlags = bufferInfo.presentationTimeUs;
|
||||||
|
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
|
||||||
|
ptsAndFlags |= PACKET_FLAG_KEY_FRAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headerBuffer.putLong(ptsAndFlags);
|
||||||
|
headerBuffer.putInt(packetSize);
|
||||||
|
headerBuffer.flip();
|
||||||
|
IO.writeFully(fd, headerBuffer);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue