[+] Camera2拍照

This commit is contained in:
acgist
2023-04-15 01:16:08 +08:00
parent ce0afef889
commit 24d849cde0
17 changed files with 565 additions and 364 deletions

View File

@@ -45,26 +45,19 @@
* `org.webrtc:google-webrtc`
* `io.github.haiyangwu:mediasoup-client`
## YUV
```
/**
* YUV终端
*
* Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
* Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
* Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
* Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
* U U U U U U V V V V V V U V U V U V V U V U V U
* V V V V V V U U U U U U U V U V U V V U V U V U
* - I420 - - YV12 - - NV12 - - NV21 -
*
* I420 = YUV420P = YU12
* NV12 = YUV420SP
*
* RGB和YUV转换算法BT.601标清、BT.709高清、BT.2020(超高清)
*
* YuvHelper
*
* @author acgist
*/
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
U U U U U U V V V V V V U V U V U V V U V U V U
V V V V V V U U U U U U U V U V U V V U V U V U
- I420 - - YV12 - - NV12 - - NV21 -
I420 = YUV420P = YU12
NV12 = YUV420SP
RGB和YUV转换算法BT.601标清、BT.709高清、BT.2020(超高清)
```

View File

@@ -121,12 +121,11 @@ public class MainActivity extends AppCompatActivity implements Serializable {
final Resources resources = this.getResources();
MediaManager.getInstance().initContext(
this.mainHandler, this.getApplicationContext(),
resources.getBoolean(R.bool.playAudio),
resources.getBoolean(R.bool.playVideo),
resources.getBoolean(R.bool.audioConsume),
resources.getBoolean(R.bool.videoConsume),
resources.getBoolean(R.bool.audioProduce),
resources.getBoolean(R.bool.videoProduce),
resources.getInteger(R.integer.imageQuantity),
resources.getString(R.string.audioQuantity),
resources.getString(R.string.videoQuantity),
resources.getInteger(R.integer.channelCount),
resources.getInteger(R.integer.iFrameInterval),
resources.getString(R.string.storagePathImage),
resources.getString(R.string.storagePathVideo),
TransportType.valueOf(resources.getString(R.string.transportType))

View File

@@ -1,8 +1,7 @@
package com.acgist.taoyao.client.signal;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.location.Criteria;
import android.location.Location;
@@ -16,8 +15,6 @@ import android.os.PowerManager;
import android.os.Process;
import android.util.Log;
import androidx.core.app.ActivityCompat;
import com.acgist.taoyao.boot.model.Header;
import com.acgist.taoyao.boot.model.Message;
import com.acgist.taoyao.boot.utils.CloseableUtils;
@@ -728,12 +725,15 @@ public final class Taoyao implements ITaoyao {
key, password,
this, this.mainHandler,
resources.getBoolean(R.bool.preview),
resources.getBoolean(R.bool.playAudio),
resources.getBoolean(R.bool.playVideo),
resources.getBoolean(R.bool.dataConsume),
resources.getBoolean(R.bool.audioConsume),
resources.getBoolean(R.bool.videoConsume),
resources.getBoolean(R.bool.audioProduce),
resources.getBoolean(R.bool.dataProduce),
resources.getBoolean(R.bool.videoProduce)
resources.getBoolean(R.bool.videoProduce),
this.mediaManager.getMediaProperties()
)
);
final boolean success = room.enter();
@@ -783,7 +783,20 @@ public final class Taoyao implements ITaoyao {
final String name = MapUtils.get(body, "name");
final String clientId = MapUtils.get(body, "clientId");
final String sessionId = MapUtils.get(body, "sessionId");
final SessionClient sessionClient = new SessionClient(sessionId, name, clientId, this, this.mainHandler);
final Resources resources = this.context.getResources();
final SessionClient sessionClient = new SessionClient(
sessionId, name, clientId, this, this.mainHandler,
resources.getBoolean(R.bool.preview),
resources.getBoolean(R.bool.playAudio),
resources.getBoolean(R.bool.playVideo),
resources.getBoolean(R.bool.dataConsume),
resources.getBoolean(R.bool.audioConsume),
resources.getBoolean(R.bool.videoConsume),
resources.getBoolean(R.bool.audioProduce),
resources.getBoolean(R.bool.dataProduce),
resources.getBoolean(R.bool.videoProduce),
this.mediaManager.getMediaProperties()
);
this.sessionClients.put(sessionId, sessionClient);
sessionClient.init();
sessionClient.offer();
@@ -884,16 +897,11 @@ public final class Taoyao implements ITaoyao {
/**
* @return 位置
*/
@SuppressLint("MissingPermission")
private Location location() {
if (this.locationManager == null) {
return null;
}
if (
ActivityCompat.checkSelfPermission(this.context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(this.context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
) {
return null;
}
final Criteria criteria = new Criteria();
criteria.setCostAllowed(false);
criteria.setBearingRequired(false);

View File

@@ -34,5 +34,8 @@
<!-- 媒体配置:是否生产视频 -->
<bool name="videoProduce">true</bool>
<integer name="imageQuantity">100</integer>
<string name="audioQuantity">fd-audio</string>
<string name="videoQuantity">fd-video</string>
<integer name="channelCount">1</integer>
<integer name="iFrameInterval">1</integer>
</resources>

View File

@@ -2,11 +2,8 @@ package com.acgist.taoyao.media;
import android.content.Context;
import android.content.Intent;
import android.media.AudioAttributes;
import android.media.ImageReader;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaRecorder;
import android.media.projection.MediaProjection;
import android.os.Handler;
import android.os.Message;
@@ -39,7 +36,6 @@ import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoCapturer;
import org.webrtc.VideoDecoderFactory;
import org.webrtc.VideoEncoderFactory;
import org.webrtc.VideoFileRenderer;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;
import org.webrtc.audio.JavaAudioDeviceModule;
@@ -71,30 +67,6 @@ public final class MediaManager {
* 当前媒体共享数量
*/
private volatile int shareClientCount;
/**
* 是否打开音频播放
*/
private boolean playAudio;
/**
* 是否打开视频播放
*/
private boolean playVideo;
/**
* 是否消费音频
*/
private boolean audioConsume;
/**
* 是否消费视频
*/
private boolean videoConsume;
/**
* 是否生产音频
*/
private boolean audioProduce;
/**
* 是否生产视频
*/
private boolean videoProduce;
/**
* 视频路径
*/
@@ -103,6 +75,20 @@ public final class MediaManager {
* 图片路径
*/
private String videoPath;
/**
* 图片质量
*/
private int imageQuantity;
/**
* 音频质量
*/
private String audioQuantity;
/**
* 视频质量
*/
private String videoQuantity;
private int channelCount;
private int iFrameInterval;
/**
* 视频来源类型
*/
@@ -243,39 +229,24 @@ public final class MediaManager {
/**
* @param mainHandler Handler
* @param context 上下文
* @param playAudio 是否播放音频
* @param playVideo 是否播放视频
* @param audioConsume 是否消费音频
* @param videoConsume 是否消费视频
* @param audioProduce 是否生产音频
* @param videoProduce 是否生产视频
*/
public void initContext(
Handler mainHandler, Context context,
boolean playAudio, boolean playVideo,
boolean audioConsume, boolean videoConsume,
boolean audioProduce, boolean videoProduce,
int imageQuantity, String audioQuantity, String videoQuantity,
int channelCount, int iFrameInterval,
String imagePath, String videoPath,
TransportType transportType
) {
this.mainHandler = mainHandler;
this.context = context;
this.playAudio = playAudio;
this.playVideo = playVideo;
this.audioConsume = audioConsume;
this.videoConsume = videoConsume;
this.audioProduce = audioProduce;
this.videoProduce = videoProduce;
this.context = context;
this.imageQuantity = imageQuantity;
this.audioQuantity = audioQuantity;
this.videoQuantity = videoQuantity;
this.channelCount = channelCount;
this.iFrameInterval = iFrameInterval;
this.imagePath = imagePath;
this.videoPath = videoPath;
this.transportType = transportType;
PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions.builder(this.context)
// .setFieldTrials("WebRTC-H264HighProfile/Enabled/")
// .setNativeLibraryName("jingle_peerconnection_so")
// .setEnableInternalTracer(true)
.createInitializationOptions()
);
}
/**
@@ -302,6 +273,7 @@ public final class MediaManager {
}
}
if (this.clientCount <= 0) {
this.initPeerConnectionFactory();
this.initMedia(videoSourceType);
this.nativeInit();
}
@@ -323,6 +295,7 @@ public final class MediaManager {
if (this.clientCount <= 0) {
this.close();
this.nativeStop();
this.stopPeerConnectionFactory();
}
return this.clientCount;
}
@@ -332,6 +305,24 @@ public final class MediaManager {
return this.recordClient != null;
}
public MediaProperties getMediaProperties() {
return this.mediaProperties;
}
private void initPeerConnectionFactory() {
PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions.builder(this.context)
// .setFieldTrials("WebRTC-H264HighProfile/Enabled/")
// .setNativeLibraryName("jingle_peerconnection_so")
// .setEnableInternalTracer(true)
.createInitializationOptions()
);
}
private void stopPeerConnectionFactory() {
PeerConnectionFactory.shutdownInternalTracer();
}
/**
* 加载媒体
*
@@ -635,16 +626,16 @@ public final class MediaManager {
public String photograph() {
synchronized (this) {
if(this.recordVideoCapturer == null) {
// 如果没有拉流不能拍照
return null;
}
String filepath;
// TODO质量读取
this.photographClient = new PhotographClient(100, this.imagePath);
if(this.clientCount <= 0) {
final MediaVideoProperties mediaVideoProperties = this.mediaProperties.getVideos().get(this.videoQuantity);
final PhotographClient photographClient = new PhotographClient(this.imageQuantity, this.imagePath);
filepath = photographClient.photograph(mediaVideoProperties.getWidth(), mediaVideoProperties.getHeight(), VideoSourceType.BACK, this.context);
return filepath;
}
this.photographClient = new PhotographClient(this.imageQuantity, this.imagePath);
if(this.recordClient == null) {
// TODO质量读取
final MediaVideoProperties mediaVideoProperties = this.mediaProperties.getVideos().get("fd-video");
final MediaVideoProperties mediaVideoProperties = this.mediaProperties.getVideos().get(this.videoQuantity);
this.recordVideoCapturer.startCapture(mediaVideoProperties.getWidth(), mediaVideoProperties.getHeight(), mediaVideoProperties.getFrameRate());
filepath = this.photographClient.waitForPhotograph();
try {
@@ -665,15 +656,16 @@ public final class MediaManager {
if(this.recordClient != null) {
return this.recordClient;
}
final MediaAudioProperties mediaAudioProperties = this.mediaProperties.getAudios().get(this.audioQuantity);
final MediaVideoProperties mediaVideoProperties = this.mediaProperties.getVideos().get(this.videoQuantity);
this.recordClient = new RecordClient(
this.videoPath,
this.taoyao,
this.mainHandler
mediaAudioProperties.getBitrate(), mediaAudioProperties.getSampleRate(), this.channelCount,
mediaVideoProperties.getBitrate(), mediaVideoProperties.getFrameRate(), this.iFrameInterval, mediaVideoProperties.getWidth(), mediaVideoProperties.getHeight(),
this.videoPath, this.taoyao, this.mainHandler
);
this.recordClient.start();
// TODO质量读取
final MediaVideoProperties mediaVideoProperties = this.mediaProperties.getVideos().get("fd-video");
this.newClient(VideoSourceType.BACK);
this.recordVideoCapturer.startCapture(mediaVideoProperties.getWidth(), mediaVideoProperties.getHeight(), mediaVideoProperties.getFrameRate());
this.recordClient.start();
return this.recordClient;
}
}
@@ -683,27 +675,25 @@ public final class MediaManager {
if(this.recordClient == null) {
return;
} else {
this.recordClient.close();
this.recordClient = null;
try {
this.recordVideoCapturer.stopCapture();
} catch (InterruptedException e) {
Log.e(MediaManager.class.getSimpleName(), "关闭视频捕获(主码流)异常", e);
}
this.recordClient.close();
this.recordClient = null;
this.closeClient();
}
}
}
/**
* @param flag Config.WHAT_*
* @param videoTrack 视频媒体流Track
* @param flag Config.WHAT_*
* @param videoTrack 视频媒体流Track
*
* @return 播放控件
*/
public SurfaceViewRenderer buildSurfaceViewRenderer(
final int flag,
final VideoTrack videoTrack
) {
public SurfaceViewRenderer buildSurfaceViewRenderer(final int flag, final VideoTrack videoTrack) {
// 预览控件
final SurfaceViewRenderer surfaceViewRenderer = new SurfaceViewRenderer(this.context);
this.mainHandler.post(() -> {
@@ -729,44 +719,14 @@ public final class MediaManager {
return surfaceViewRenderer;
}
public void pauseAudio() {
synchronized (this.mediaStream.audioTracks) {
this.mediaStream.audioTracks.forEach(a -> a.setEnabled(false));
}
}
public void resumeAudio() {
synchronized (this.mediaStream.audioTracks) {
this.mediaStream.audioTracks.forEach(a -> a.setEnabled(true));
}
}
public void pauseVideo() {
synchronized (this.mediaStream.videoTracks) {
this.mediaStream.videoTracks.forEach(v -> v.setEnabled(false));
}
synchronized (this.mediaStream.preservedVideoTracks) {
this.mediaStream.preservedVideoTracks.forEach(v -> v.setEnabled(false));
}
}
public void resumeVideo() {
synchronized (this.mediaStream.videoTracks) {
this.mediaStream.videoTracks.forEach(v -> v.setEnabled(true));
}
synchronized (this.mediaStream.preservedVideoTracks) {
this.mediaStream.preservedVideoTracks.forEach(v -> v.setEnabled(true));
}
}
/**
* 关闭声音
*/
private void closeAudio() {
if(this.audioTrack != null) {
this.audioTrack.dispose();
this.audioTrack = null;
}
// if(this.audioTrack != null) {
// this.audioTrack.dispose();
// this.audioTrack = null;
// }
if(this.audioSource != null) {
this.audioSource.dispose();
this.audioSource = null;
@@ -777,10 +737,10 @@ public final class MediaManager {
* 关闭视频
*/
private void closeVideo() {
if(this.videoTrack != null) {
this.videoTrack.dispose();
this.videoTrack = null;
}
// if(this.videoTrack != null) {
// this.videoTrack.dispose();
// this.videoTrack = null;
// }
if(this.videoSource != null) {
this.videoSource.dispose();
this.videoSource = null;
@@ -789,6 +749,9 @@ public final class MediaManager {
this.videoCapturer.dispose();
this.videoCapturer = null;
}
}
private void closeRecord() {
if(this.recordVideoTrack != null) {
this.recordVideoTrack.dispose();
this.recordVideoTrack = null;
@@ -801,10 +764,6 @@ public final class MediaManager {
this.recordVideoCapturer.dispose();
this.recordVideoCapturer = null;
}
if(this.surfaceTextureHelper != null) {
this.surfaceTextureHelper.dispose();
this.surfaceTextureHelper = null;
}
}
private void closeMedia() {
@@ -813,10 +772,14 @@ public final class MediaManager {
this.eglBase = null;
this.shareEglContext = null;
}
// if (this.mediaStream != null) {
// this.mediaStream.dispose();
// this.mediaStream = null;
// }
if (this.mediaStream != null) {
this.mediaStream.dispose();
this.mediaStream = null;
}
if(this.surfaceTextureHelper != null) {
this.surfaceTextureHelper.dispose();
this.surfaceTextureHelper = null;
}
if (this.peerConnectionFactory != null) {
this.peerConnectionFactory.dispose();
this.peerConnectionFactory = null;
@@ -829,9 +792,8 @@ public final class MediaManager {
private void close() {
this.closeAudio();
this.closeVideo();
this.closeRecord();
this.closeMedia();
// PeerConnectionFactory.shutdownInternalTracer();
// PeerConnectionFactory.stopInternalTracingCapture();
}
/**

View File

@@ -1,24 +1,42 @@
package com.acgist.taoyao.media.client;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.OutputConfiguration;
import android.hardware.camera2.params.SessionConfiguration;
import android.media.Image;
import android.media.ImageReader;
import android.os.Environment;
import android.util.Log;
import android.view.Surface;
import com.acgist.taoyao.boot.utils.DateUtils;
import com.acgist.taoyao.media.VideoSourceType;
import org.webrtc.VideoFrame;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 拍照终端
@@ -30,19 +48,35 @@ public class PhotographClient {
private final int quantity;
private final String filename;
private final String filepath;
private volatile boolean wait;
private volatile boolean finish;
private Surface surface;
private ImageReader imageReader;
private CameraDevice cameraDevice;
private CameraManager cameraManager;
private CameraCaptureSession cameraCaptureSession;
public PhotographClient(int quantity, String path) {
this.quantity = quantity;
this.filename = DateUtils.format(LocalDateTime.now(), DateUtils.DateTimeStyle.YYYYMMDDHH24MMSS) + ".jpg";
this.filepath = Paths.get(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath(), path, this.filename).toString();
this.wait = true;
this.finish = false;
Log.i(RecordClient.class.getSimpleName(), "拍摄照片文件:" + this.filepath);
}
// ================ 拉流拍照 ================ //
public String photograph(VideoFrame videoFrame) {
final Thread thread = new Thread(() -> this.photographBackground(videoFrame));
thread.setName("PhotographThread");
thread.setDaemon(true);
thread.start();
if(this.wait) {
this.wait = false;
final Thread thread = new Thread(() -> this.photographBackground(videoFrame));
thread.setName("PhotographThread");
thread.setDaemon(true);
thread.start();
} else {
videoFrame.release();
}
return this.filepath;
}
@@ -59,7 +93,8 @@ public class PhotographClient {
final YuvImage image = this.i420ToYuvImage(i420, width, height);
i420.release();
videoFrame.release();
image.compressToJpeg(new Rect(0, 0, width, height), this.quantity, byteArray);
final Rect rect = new Rect(0, 0, width, height);
image.compressToJpeg(rect, this.quantity, byteArray);
final byte[] array = byteArray.toByteArray();
final Bitmap bitmap = BitmapFactory.decodeByteArray(array, 0, array.length);
// final Matrix matrix = new Matrix();
@@ -69,13 +104,21 @@ public class PhotographClient {
} catch (Exception e) {
Log.e(PhotographClient.class.getSimpleName(), "拍照异常", e);
}
this.notifyWait();
}
private void notifyWait() {
synchronized (this) {
this.finish = true;
this.notifyAll();
}
}
public String waitForPhotograph() {
synchronized (this) {
if(this.finish) {
return this.filepath;
}
try {
this.wait(5000);
} catch (InterruptedException e) {
@@ -86,60 +129,133 @@ public class PhotographClient {
}
private YuvImage i420ToYuvImage(VideoFrame.I420Buffer i420, int width, int height) {
final ByteBuffer[] yuvPlanes = new ByteBuffer[] {
i420.getDataY(), i420.getDataU(), i420.getDataV()
};
final int[] yuvStrides = new int[] {
i420.getStrideY(), i420.getStrideU(), i420.getStrideV()
};
if (
yuvStrides[0] != width ||
yuvStrides[1] != width / 2 ||
yuvStrides[2] != width / 2
) {
return i420ToYuvImage(yuvPlanes, yuvStrides, width, height);
}
final byte[] bytes = new byte[yuvStrides[0] * height + yuvStrides[1] * height / 2 + yuvStrides[2] * height / 2];
final ByteBuffer yBuffer = ByteBuffer.wrap(bytes, 0, width * height);
this.copyPlane(yuvPlanes[0], yBuffer);
final byte[] uvBytes = new byte[width / 2 * height / 2];
final ByteBuffer uvBuffer = ByteBuffer.wrap(uvBytes, 0, uvBytes.length);
this.copyPlane(yuvPlanes[2], uvBuffer);
for (int row = 0; row < height / 2; row++) {
for (int col = 0; col < width / 2; col++) {
bytes[width * height + row * width + col * 2] = uvBytes[row * width / 2 + col];
}
}
this.copyPlane(yuvPlanes[1], uvBuffer);
for (int row = 0; row < height / 2; row++) {
for (int col = 0; col < width / 2; col++) {
bytes[width * height + row * width + col * 2 + 1] = uvBytes[row * width / 2 + col];
}
}
return new YuvImage(bytes, ImageFormat.NV21, width, height, null);
}
private YuvImage i420ToYuvImage(ByteBuffer[] yuvPlanes, int[] yuvStrides, int width, int height) {
int i = 0;
final byte[] bytes = new byte[width * height * 3 / 2];
int index = 0;
final int yy = i420.getStrideY();
final int uu = i420.getStrideU();
final int vv = i420.getStrideV();
final ByteBuffer y = i420.getDataY();
final ByteBuffer u = i420.getDataU();
final ByteBuffer v = i420.getDataV();
final byte[] nv21 = new byte[width * height * 3 / 2];
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
bytes[i++] = yuvPlanes[0].get(col + row * yuvStrides[0]);
nv21[index++] = y.get(col + row * yy);
}
}
for (int row = 0; row < height / 2; row++) {
for (int col = 0; col < width / 2; col++) {
bytes[i++] = yuvPlanes[2].get(col + row * yuvStrides[2]);
bytes[i++] = yuvPlanes[1].get(col + row * yuvStrides[1]);
final int halfWidth = width / 2;
final int halfHeight = height / 2;
for (int row = 0; row < halfHeight; row++) {
for (int col = 0; col < halfWidth; col++) {
nv21[index++] = v.get(col + row * vv);
nv21[index++] = u.get(col + row * uu);
}
}
return new YuvImage(bytes, ImageFormat.NV21, width, height, null);
return new YuvImage(nv21, ImageFormat.NV21, width, height, null);
}
private void copyPlane(ByteBuffer src, ByteBuffer dst) {
src.position(0).limit(src.capacity());
dst.put(src);
dst.position(0).limit(dst.capacity());
// ================ Camera2拍照 ================ //
@SuppressLint("MissingPermission")
public String photograph(int width, int height, VideoSourceType type, Context context) {
this.cameraManager = context.getSystemService(CameraManager.class);
this.imageReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 1);
this.surface = this.imageReader.getSurface();
this.imageReader.setOnImageAvailableListener(this.imageAvailableListener, null);
try {
final String cameraId = String.valueOf(type == VideoSourceType.BACK ? CameraCharacteristics.LENS_FACING_BACK : CameraCharacteristics.LENS_FACING_FRONT);
this.cameraManager.openCamera(cameraId, this.cameraDeviceStateCallback, null);
} catch (CameraAccessException e) {
Log.e(PhotographClient.class.getSimpleName(), "拍照异常", e);
PhotographClient.this.closeCamera();
}
return this.filepath;
}
private ImageReader.OnImageAvailableListener imageAvailableListener = new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader imageReader) {
final Image image = imageReader.acquireLatestImage();
final ByteBuffer byteBuffer = image.getPlanes()[0].getBuffer();
final byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
final File file = new File(PhotographClient.this.filepath);
try (
final OutputStream output = new FileOutputStream(file);
) {
output.write(bytes,0,bytes.length);
} catch (IOException e) {
Log.e(PhotographClient.class.getSimpleName(), "拍照异常", e);
PhotographClient.this.closeCamera();
} finally {
image.close();
PhotographClient.this.closeCamera();
}
}
};
private CameraDevice.StateCallback cameraDeviceStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice cameraDevice) {
PhotographClient.this.cameraDevice = cameraDevice;
try {
PhotographClient.this.cameraDevice.createCaptureSession(new SessionConfiguration(
SessionConfiguration.SESSION_REGULAR,
List.of(new OutputConfiguration(PhotographClient.this.surface)),
Runnable::run,
PhotographClient.this.cameraCaptureSessionStateCallback
));
} catch (CameraAccessException e) {
Log.e(PhotographClient.class.getSimpleName(), "拍照异常", e);
PhotographClient.this.closeCamera();
}
}
@Override
public void onDisconnected(CameraDevice cameraDevice) {
PhotographClient.this.closeCamera();
}
@Override
public void onError(CameraDevice cameraDevice, int error) {
PhotographClient.this.closeCamera();
}
};
private CameraCaptureSession.StateCallback cameraCaptureSessionStateCallback = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession cameraCaptureSession) {
try {
PhotographClient.this.cameraCaptureSession = cameraCaptureSession;
final CaptureRequest.Builder builder = PhotographClient.this.cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
builder.addTarget(PhotographClient.this.surface);
cameraCaptureSession.setRepeatingRequest(builder.build(), null, null);
} catch (CameraAccessException e) {
Log.e(PhotographClient.class.getSimpleName(), "拍照异常", e);
PhotographClient.this.closeCamera();
}
}
@Override
public void onConfigureFailed(CameraCaptureSession session) {
PhotographClient.this.closeCamera();
}
};
private void closeCamera() {
if(this.cameraCaptureSession != null) {
this.cameraCaptureSession.close();
this.cameraCaptureSession = null;
}
if(this.cameraDevice != null) {
this.cameraDevice.close();
this.cameraDevice = null;
}
// 最后释放ImageReader
if(this.surface != null) {
this.surface.release();
this.surface = null;
}
if(this.imageReader != null) {
this.imageReader.close();
this.imageReader = null;
}
}
}

View File

@@ -1,7 +1,5 @@
package com.acgist.taoyao.media.client;
import android.graphics.YuvImage;
import android.media.AudioFormat;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
@@ -10,18 +8,13 @@ import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.view.Surface;
import com.acgist.taoyao.boot.utils.DateUtils;
import com.acgist.taoyao.media.MediaManager;
import com.acgist.taoyao.media.VideoSourceType;
import com.acgist.taoyao.media.signal.ITaoyao;
import org.webrtc.EglBase;
import org.webrtc.GlRectDrawer;
import org.webrtc.TextureBufferImpl;
import org.webrtc.VideoFrame;
import org.webrtc.VideoFrameDrawer;
import org.webrtc.VideoSink;
import org.webrtc.YuvHelper;
import org.webrtc.audio.JavaAudioDeviceModule;
@@ -29,34 +22,22 @@ import org.webrtc.audio.JavaAudioDeviceModule;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.Queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* 录像机
*
* https://www.freesion.com/article/448330501/
* https://blog.csdn.net/nanoage/article/details/127406494
* https://webrtc.org.cn/20190419_tutorial3_webrtc_android
* https://blog.csdn.net/CSDN_Mew/article/details/103406781
* https://blog.csdn.net/Tong_Hou/article/details/112116349
* https://blog.csdn.net/u011418943/article/details/127108642
* https://blog.csdn.net/m0_60259116/article/details/126875532
* https://blog.csdn.net/csdn_shen0221/article/details/119982257
* https://blog.csdn.net/csdn_shen0221/article/details/120331004
* https://github.com/flutter-webrtc/flutter-webrtc/blob/main/android/src/main/java/com/cloudwebrtc/webrtc/record/VideoFileRenderer.java
*
* @author acgist
*/
public class RecordClient extends Client implements VideoSink, JavaAudioDeviceModule.SamplesReadyCallback {
private static final long WAIT_TIME_MS = 50;
private static final long WAIT_TIME_US = WAIT_TIME_MS * 1000;
/**
* 音频准备录制
*/
@@ -73,6 +54,40 @@ public class RecordClient extends Client implements VideoSink, JavaAudioDeviceMo
* 录制文件路径
*/
private final String filepath;
/**
* 比特率96 * 1000 | 128 * 1000 | 256 * 1000
* 比特率96 * 1024 | 128 * 1024 | 256 * 1024
*/
private final int audioBitRate;
/**
* 采样率32000 | 44100 | 48000
*/
private final int sampleRate;
/**
* 通道数量1 | 2
*/
private final int channelCount;
/**
* 比特率800 * 1000 | 1600 * 1000 | 2500 * 1000
* 比特率800 * 1024 | 1600 * 1024 | 2500 * 1024
*/
private final int videoBitRate;
/**
* 帧率15 | 20 | 25 | 30
*/
private final int frameRate;
/**
* 关键帧频率1 ~ 5
*/
private final int iFrameInterval;
/**
* 宽度1920
*/
private final int width;
/**
* 高度1080
*/
private final int height;
/**
* 音频编码
*/
@@ -101,11 +116,29 @@ public class RecordClient extends Client implements VideoSink, JavaAudioDeviceMo
* 媒体合成器
*/
private MediaMuxer mediaMuxer;
/**
* 音频队列
*/
private final BlockingQueue<JavaAudioDeviceModule.AudioSamples> audioSamplesQueue;
/**
* 视频队列
*/
private final BlockingQueue<VideoFrame> videoFrameQueue;
public RecordClient(String path, ITaoyao taoyao, Handler mainHandler) {
public RecordClient(
int audioBitRate, int sampleRate, int channelCount,
int videoBitRate, int frameRate, int iFrameInterval, int width, int height,
String path, ITaoyao taoyao, Handler mainHandler
) {
super("本地录像", "LocalRecordClient", taoyao, mainHandler);
this.audioBitRate = audioBitRate * 1024;
this.sampleRate = sampleRate;
this.channelCount = channelCount;
this.videoBitRate = videoBitRate * 1024;
this.frameRate = frameRate;
this.iFrameInterval = iFrameInterval;
this.width = width;
this.height = height;
this.filename = DateUtils.format(LocalDateTime.now(), DateUtils.DateTimeStyle.YYYYMMDDHH24MMSS) + ".mp4";
this.filepath = Paths.get(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath(), path, this.filename).toString();
this.audioSamplesQueue = new LinkedBlockingQueue<>();
@@ -119,36 +152,29 @@ public class RecordClient extends Client implements VideoSink, JavaAudioDeviceMo
}
Log.i(RecordClient.class.getSimpleName(), "录制视频文件:" + this.filepath);
super.init();
this.mediaManager.newClient(VideoSourceType.BACK);
this.record(null, null, 1, 1);
}
}
private void record(String audioFormat, String videoFormat, int width, int height) {
if (
this.audioThread == null || !this.audioThread.isAlive() ||
this.videoThread == null || !this.videoThread.isAlive()
) {
this.initMediaMuxer();
this.initAudioThread(MediaFormat.MIMETYPE_AUDIO_AAC, 96000, 44100, 1);
this.initVideoThread(MediaFormat.MIMETYPE_VIDEO_AVC, 2500 * 1000, 30, 1, 1920, 1080);
if (
this.audioThread == null || !this.audioThread.isAlive() ||
this.videoThread == null || !this.videoThread.isAlive()
) {
this.initMediaMuxer();
this.initAudioThread(MediaFormat.MIMETYPE_AUDIO_AAC);
this.initVideoThread(MediaFormat.MIMETYPE_VIDEO_AVC);
}
}
}
/**
* @param audioType 类型
* @param bitRate 比特率96 * 1000 | 128 * 1000 | 256 * 1000
* @param sampleRate 采样率32000 | 44100 | 48000
* @param channelCount 通道数量
* @param audioType 类型
*/
private void initAudioThread(String audioType, int bitRate, int sampleRate, int channelCount) {
private void initAudioThread(String audioType) {
try {
final MediaFormat audioFormat = MediaFormat.createAudioFormat(audioType, sampleRate, channelCount);
// audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, AudioFormat.ENCODING_PCM_16BIT);
final MediaFormat audioFormat = MediaFormat.createAudioFormat(audioType, this.sampleRate, this.channelCount);
// audioFormat.setString(MediaFormat.KEY_MIME, audioType);
// audioFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, this.sampleRate);
// audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, this.channelCount);
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, this.audioBitRate);
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
// audioFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 8 * 1024);
// audioFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
this.audioCodec = MediaCodec.createEncoderByType(audioType);
this.audioCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (Exception e) {
@@ -160,48 +186,45 @@ public class RecordClient extends Client implements VideoSink, JavaAudioDeviceMo
this.audioHandler.post(this::audioCodec);
}
private volatile long audioPts = 0;
private void audioCodec() {
long pts = 0L;
int trackIndex = -1;
int outputIndex;
long pts = 0L;
this.audioCodec.start();
this.audioActive = true;
JavaAudioDeviceModule.AudioSamples audioSamples = null;
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!this.close) {
try {
audioSamples = this.audioSamplesQueue.poll(1000, TimeUnit.MILLISECONDS);
audioSamples = this.audioSamplesQueue.poll(WAIT_TIME_MS, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Log.e(RecordClient.class.getSimpleName(), "录制线程等待异常", e);
}
if(audioSamples == null) {
continue;
}
int index = this.audioCodec.dequeueInputBuffer(1000L * 1000);
int index = this.audioCodec.dequeueInputBuffer(WAIT_TIME_US);
if (index >= 0) {
final byte[] data = audioSamples.getData();
final ByteBuffer buffer = this.audioCodec.getInputBuffer(index);
buffer.put(data);
this.audioCodec.queueInputBuffer(index, 0, data.length, this.audioPts, 0);
this.audioPts += data.length * 125 / 12; // 1000000 microseconds / 48000hz / 2 bytes
// presTime += data.length * 125 / 12; // 1000000 microseconds / 48000hz / 2 bytes
// presTime += data.length * (1_000_000 / audioSamples.getSampleRate() / 2); //16位最后那个数字是28位是1
// 1000000 microseconds / 48000 hz / 2 bytes
this.audioPts += data.length * (1_000_000 / audioSamples.getSampleRate() / 2);
} else {
// WARN
}
audioSamples = null;
outputIndex = this.audioCodec.dequeueOutputBuffer(bufferInfo, 1000L * 1000);
outputIndex = this.audioCodec.dequeueOutputBuffer(bufferInfo, WAIT_TIME_US);
if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
// } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
} else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
synchronized (this) {
trackIndex = this.mediaMuxer.addTrack(this.audioCodec.getOutputFormat());
Log.i(RecordClient.class.getSimpleName(), "开始录制音频:" + trackIndex);
if (this.videoActive) {
if (!this.close && this.videoActive) {
Log.i(RecordClient.class.getSimpleName(), "开始录制文件:" + this.filename);
this.mediaMuxer.start();
this.notifyAll();
@@ -230,15 +253,15 @@ public class RecordClient extends Client implements VideoSink, JavaAudioDeviceMo
// } else if (bufferInfo.flags & MediaCodec.BUFFER_FLAG_PARTIAL_FRAME == MediaCodec.BUFFER_FLAG_PARTIAL_FRAME) {
// }
if((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
// this.close();
this.close();
break;
}
} else {
// WARN
}
}
synchronized (this) {
if (this.audioCodec != null) {
if (this.audioCodec != null && this.audioActive) {
Log.i(RecordClient.class.getSimpleName(), "结束录制音频");
this.audioCodec.stop();
this.audioCodec.release();
@@ -247,15 +270,13 @@ public class RecordClient extends Client implements VideoSink, JavaAudioDeviceMo
this.audioActive = false;
if (this.mediaMuxer != null && !this.videoActive) {
Log.i(RecordClient.class.getSimpleName(), "结束录制文件:" + this.filename);
this.mediaMuxer.stop();
// this.mediaMuxer.stop();
this.mediaMuxer.release();
this.mediaMuxer = null;
}
}
}
private volatile long audioPts = 0;
/**
* @param audioSamples PCM数据
*/
@@ -272,25 +293,18 @@ public class RecordClient extends Client implements VideoSink, JavaAudioDeviceMo
}
/**
* @param videoType 视频格式
* @param bitRate 比特率800 * 1000 | 1600 * 1000 | 2500 * 1000
* @param frameRate 帧率30
* @param iFrameInterval 关键帧频率1 ~ 5
* @param width 宽度1920
* @param height 高度1080
* @param videoType 视频格式
*/
private void initVideoThread(String videoType, int bitRate, int frameRate, int iFrameInterval, int width, int height) {
private void initVideoThread(String videoType) {
try {
final MediaFormat videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 1920, 1080);
final MediaFormat videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, this.width, this.height);
// videoFormat.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel31);
// videoFormat.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
// videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 1920 * 1080 * 5);
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 6_000_000);
// videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 800 * 1000);
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
// videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, this.videoBitRate);
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, this.frameRate);
// videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, this.iFrameInterval);
this.videoCodec = MediaCodec.createEncoderByType(videoType);
this.videoCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (Exception e) {
@@ -303,52 +317,39 @@ public class RecordClient extends Client implements VideoSink, JavaAudioDeviceMo
}
private void videoCodec() {
int trackIndex = -1;
long pts = 0L;
int trackIndex = -1;
int outputIndex;
long pts = 0L;
VideoFrame videoFrame = null;
this.videoCodec.start();
this.videoActive = true;
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
VideoFrame videoFrame = null;
while (!this.close) {
try {
videoFrame = this.videoFrameQueue.poll(1000, TimeUnit.MILLISECONDS);
videoFrame = this.videoFrameQueue.poll(WAIT_TIME_MS, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Log.e(RecordClient.class.getSimpleName(), "录制线程等待异常", e);
}
if(videoFrame == null) {
continue;
}
final TextureBufferImpl buffer = (TextureBufferImpl) videoFrame.getBuffer();
final int outputFrameSize = videoFrame.getRotatedWidth() * videoFrame.getRotatedHeight() * 3 / 2;
// final ByteBuffer outputFrameBuffer = ByteBuffer.allocateDirect(outputFrameSize);
final int index = this.videoCodec.dequeueInputBuffer(1000L * 1000);
VideoFrame.I420Buffer i420 = buffer.toI420();
final ByteBuffer bufferx = this.videoCodec.getInputBuffer(index);
// YuvHelper.I420Copy(i420.getDataY(), i420.getStrideY(), i420.getDataU(), i420.getStrideU(), i420.getDataV(), i420.getStrideV(), outputFrameBuffer, i420.getWidth(), i420.getHeight());
// YuvHelper.I420Rotate(i420.getDataY(), i420.getStrideY(), i420.getDataU(), i420.getStrideU(), i420.getDataV(), i420.getStrideV(), outputFrameBuffer, i420.getWidth(), i420.getHeight(), videoFrame.getRotation());
YuvHelper.I420ToNV12(i420.getDataY(), i420.getStrideY(), i420.getDataU(), i420.getStrideU(), i420.getDataV(), i420.getStrideV(), bufferx, i420.getWidth(), i420.getHeight());
final int videoFrameSize = videoFrame.getRotatedWidth() * videoFrame.getRotatedHeight() * 3 / 2;
final int index = this.videoCodec.dequeueInputBuffer(WAIT_TIME_US);
final VideoFrame.I420Buffer i420 = videoFrame.getBuffer().toI420();
final ByteBuffer inputByteBuffer = this.videoCodec.getInputBuffer(index);
YuvHelper.I420ToNV12(i420.getDataY(), i420.getStrideY(), i420.getDataU(), i420.getStrideU(), i420.getDataV(), i420.getStrideV(), inputByteBuffer, i420.getWidth(), i420.getHeight());
i420.release();
videoFrame.release();
// bufferx.put(outputFrameBuffer.array());
this.videoCodec.queueInputBuffer(index, 0, outputFrameSize, videoFrame.getTimestampNs(), 0);
// this.videoFrameDrawer.drawFrame(videoFrame, this.glRectDrawer, null, 0, 0, videoFrame.getRotatedWidth(), videoFrame.getRotatedHeight());
// videoFrame.release();
// videoFrame = null;
// this.eglBase.swapBuffers();
outputIndex = this.videoCodec.dequeueOutputBuffer(bufferInfo, 1000L * 1000);
this.videoCodec.queueInputBuffer(index, 0, videoFrameSize, videoFrame.getTimestampNs(), 0);
videoFrame = null;
outputIndex = this.videoCodec.dequeueOutputBuffer(bufferInfo, WAIT_TIME_US);
if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
// } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
} else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
synchronized (this) {
trackIndex = this.mediaMuxer.addTrack(this.videoCodec.getOutputFormat());
Log.i(RecordClient.class.getSimpleName(), "开始录制视频:" + trackIndex);
if (this.audioActive) {
if (!this.close && this.audioActive) {
Log.i(RecordClient.class.getSimpleName(), "开始录制文件:" + this.filename);
this.mediaMuxer.start();
this.notifyAll();
@@ -358,6 +359,7 @@ public class RecordClient extends Client implements VideoSink, JavaAudioDeviceMo
} catch (InterruptedException e) {
Log.e(RecordClient.class.getSimpleName(), "录制线程等待异常", e);
}
} else {
}
}
} else if (outputIndex >= 0) {
@@ -378,14 +380,15 @@ public class RecordClient extends Client implements VideoSink, JavaAudioDeviceMo
// } else if (bufferInfo.flags & MediaCodec.BUFFER_FLAG_PARTIAL_FRAME == MediaCodec.BUFFER_FLAG_PARTIAL_FRAME) {
// }
if((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
// this.close();
this.close();
break;
}
} else {
// WARN
}
}
synchronized (this) {
if (this.videoCodec != null) {
if (this.videoCodec != null && this.videoActive) {
Log.i(RecordClient.class.getSimpleName(), "结束录制视频");
this.videoCodec.stop();
this.videoCodec.release();
@@ -394,7 +397,7 @@ public class RecordClient extends Client implements VideoSink, JavaAudioDeviceMo
this.videoActive = false;
if (this.mediaMuxer != null && !this.audioActive) {
Log.i(RecordClient.class.getSimpleName(), "结束录制文件:" + this.filename);
this.mediaMuxer.stop();
// this.mediaMuxer.stop();
this.mediaMuxer.release();
this.mediaMuxer = null;
}
@@ -440,8 +443,12 @@ public class RecordClient extends Client implements VideoSink, JavaAudioDeviceMo
if (this.videoThread != null) {
this.videoThread.quitSafely();
}
final File file = new File(this.filepath);
if(file.length() <= 0) {
Log.i(RecordClient.class.getSimpleName(), "删除没有录制数据文件:" + this.filepath);
file.delete();
}
this.notifyAll();
this.mediaManager.closeClient();
}
}

View File

@@ -10,6 +10,9 @@ import com.acgist.taoyao.boot.utils.PointerUtils;
import com.acgist.taoyao.media.RouterCallback;
import com.acgist.taoyao.media.VideoSourceType;
import com.acgist.taoyao.media.config.Config;
import com.acgist.taoyao.media.config.MediaAudioProperties;
import com.acgist.taoyao.media.config.MediaProperties;
import com.acgist.taoyao.media.config.MediaVideoProperties;
import com.acgist.taoyao.media.signal.ITaoyao;
import org.webrtc.AudioTrack;
@@ -36,27 +39,49 @@ public class Room extends CloseableClient implements RouterCallback {
private final String roomId;
private final String password;
private final boolean preview;
private final boolean playAudio;
private final boolean playVideo;
private final boolean dataConsume;
private final boolean audioConsume;
private final boolean videoConsume;
private final boolean dataProduce;
private final boolean audioProduce;
private final boolean videoProduce;
private final MediaProperties mediaProperties;
private final Map<String, RemoteClient> remoteClients;
private final long nativeRoomPointer;
private LocalClient localClient;
private Map<String, RemoteClient> remoteClients;
private PeerConnection.RTCConfiguration rtcConfiguration;
private PeerConnectionFactory peerConnectionFactory;
private String rtpCapabilities;
private String sctpCapabilities;
/**
*
* @param name
* @param clientId
* @param roomId
* @param password
* @param taoyao
* @param mainHandler
* @param preview 是否预览视频
* @param playAudio 是否播放音频
* @param playVideo 是否播放视频
* @param dataConsume 是否消费数据
* @param audioConsume 是否消费音频
* @param videoConsume 是否消费视频
* @param dataProduce 是否生产数据
* @param audioProduce 是否生产音频
* @param videoProduce 是否生产视频
*/
public Room(
String name, String clientId,
String roomId, String password,
ITaoyao taoyao, Handler mainHandler,
boolean preview,
boolean preview, boolean playAudio, boolean playVideo,
boolean dataConsume, boolean audioConsume, boolean videoConsume,
boolean dataProduce, boolean audioProduce, boolean videoProduce
boolean dataProduce, boolean audioProduce, boolean videoProduce,
MediaProperties mediaProperties
) {
super(taoyao, mainHandler);
this.name = name;
@@ -64,14 +89,17 @@ public class Room extends CloseableClient implements RouterCallback {
this.roomId = roomId;
this.password = password;
this.preview = preview;
this.playAudio = playAudio;
this.playVideo = playVideo;
this.dataConsume = dataConsume;
this.audioConsume = audioConsume;
this.videoConsume = videoConsume;
this.dataProduce = dataProduce;
this.audioProduce = audioProduce;
this.videoProduce = videoProduce;
this.nativeRoomPointer = this.nativeNewRoom(roomId, this);
this.mediaProperties = mediaProperties;
this.remoteClients = new ConcurrentHashMap<>();
this.nativeRoomPointer = this.nativeNewRoom(roomId, this);
}
public boolean enter() {

View File

@@ -8,6 +8,7 @@ import com.acgist.taoyao.boot.utils.ListUtils;
import com.acgist.taoyao.boot.utils.MapUtils;
import com.acgist.taoyao.media.VideoSourceType;
import com.acgist.taoyao.media.config.Config;
import com.acgist.taoyao.media.config.MediaProperties;
import com.acgist.taoyao.media.signal.ITaoyao;
import org.webrtc.DataChannel;
@@ -37,6 +38,16 @@ public class SessionClient extends Client {
* 会话ID
*/
private final String sessionId;
private final boolean preview;
private final boolean playAudio;
private final boolean playVideo;
private final boolean dataConsume;
private final boolean audioConsume;
private final boolean videoConsume;
private final boolean dataProduce;
private final boolean audioProduce;
private final boolean videoProduce;
private final MediaProperties mediaProperties;
/**
* 本地媒体
*/
@@ -62,9 +73,25 @@ public class SessionClient extends Client {
*/
private PeerConnectionFactory peerConnectionFactory;
public SessionClient(String sessionId, String name, String clientId, ITaoyao taoyao, Handler mainHandler) {
public SessionClient(
String sessionId, String name, String clientId, ITaoyao taoyao, Handler mainHandler,
boolean preview, boolean playAudio, boolean playVideo,
boolean dataConsume, boolean audioConsume, boolean videoConsume,
boolean dataProduce, boolean audioProduce, boolean videoProduce,
MediaProperties mediaProperties
) {
super(name, clientId, taoyao, mainHandler);
this.sessionId = sessionId;
this.preview = preview;
this.playAudio = playAudio;
this.playVideo = playVideo;
this.dataConsume = dataConsume;
this.audioConsume = audioConsume;
this.videoConsume = videoConsume;
this.dataProduce = dataProduce;
this.audioProduce = audioProduce;
this.videoProduce = videoProduce;
this.mediaProperties = mediaProperties;
}
@Override

View File

@@ -26,7 +26,11 @@ public class MediaAudioProperties {
*/
private Format format;
/**
* 采样数8|16|32
* 比特率96|128|256
*/
private Integer bitrate;
/**
* 采样位数8|16|32
*/
private Integer sampleSize;
/**
@@ -42,6 +46,14 @@ public class MediaAudioProperties {
this.format = format;
}
public Integer getBitrate() {
return bitrate;
}
public void setBitrate(Integer bitrate) {
this.bitrate = bitrate;
}
public Integer getSampleSize() {
return this.sampleSize;
}

View File

@@ -25,14 +25,6 @@ public class MediaProperties {
* 最大视频高度
*/
private Integer maxHeight;
/**
* 最小视频码率
*/
private Integer minBitrate;
/**
* 最大视频码率
*/
private Integer maxBitrate;
/**
* 最小视频帧率
*/
@@ -42,11 +34,19 @@ public class MediaProperties {
*/
private Integer maxFrameRate;
/**
* 最小音频采样数
* 最小视频码率
*/
private Integer minVideoBitrate;
/**
* 最大视频码率
*/
private Integer maxVideoBitrate;
/**
* 最小音频采样位数
*/
private Integer minSampleSize;
/**
* 最大音频采样数
* 最大音频采样
*/
private Integer maxSampleSize;
/**
@@ -57,6 +57,14 @@ public class MediaProperties {
* 最大音频采样率
*/
private Integer maxSampleRate;
/**
* 最小音频码率
*/
private Integer minAudioBitrate;
/**
* 最大音频码率
*/
private Integer maxAudioBitrate;
/**
* 音频默认配置
*/
@@ -106,22 +114,6 @@ public class MediaProperties {
this.maxHeight = maxHeight;
}
public Integer getMinBitrate() {
return minBitrate;
}
public void setMinBitrate(Integer minBitrate) {
this.minBitrate = minBitrate;
}
public Integer getMaxBitrate() {
return maxBitrate;
}
public void setMaxBitrate(Integer maxBitrate) {
this.maxBitrate = maxBitrate;
}
public Integer getMinFrameRate() {
return minFrameRate;
}
@@ -138,6 +130,22 @@ public class MediaProperties {
this.maxFrameRate = maxFrameRate;
}
public Integer getMinVideoBitrate() {
return minVideoBitrate;
}
public void setMinVideoBitrate(Integer minVideoBitrate) {
this.minVideoBitrate = minVideoBitrate;
}
public Integer getMaxVideoBitrate() {
return maxVideoBitrate;
}
public void setMaxVideoBitrate(Integer maxVideoBitrate) {
this.maxVideoBitrate = maxVideoBitrate;
}
public Integer getMinSampleSize() {
return minSampleSize;
}
@@ -170,6 +178,22 @@ public class MediaProperties {
this.maxSampleRate = maxSampleRate;
}
public Integer getMinAudioBitrate() {
return minAudioBitrate;
}
public void setMinAudioBitrate(Integer minAudioBitrate) {
this.minAudioBitrate = minAudioBitrate;
}
public Integer getMaxAudioBitrate() {
return maxAudioBitrate;
}
public void setMaxAudioBitrate(Integer maxAudioBitrate) {
this.maxAudioBitrate = maxAudioBitrate;
}
public MediaAudioProperties getAudio() {
return audio;
}
@@ -201,4 +225,5 @@ public class MediaProperties {
public void setVideos(Map<String, MediaVideoProperties> videos) {
this.videos = videos;
}
}