diff --git a/taoyao-client-android/README.md b/taoyao-client-android/README.md index 9cf0122..7832e12 100644 --- a/taoyao-client-android/README.md +++ b/taoyao-client-android/README.md @@ -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(超高清) ``` diff --git a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MainActivity.java b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MainActivity.java index 1b10d0a..c4a5c3c 100644 --- a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MainActivity.java +++ b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MainActivity.java @@ -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)) diff --git a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/signal/Taoyao.java b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/signal/Taoyao.java index bd7e167..e1ed1f6 100644 --- a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/signal/Taoyao.java +++ b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/signal/Taoyao.java @@ -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); diff --git a/taoyao-client-android/taoyao/client/src/main/res/values/settings.xml b/taoyao-client-android/taoyao/client/src/main/res/values/settings.xml index 2dc7863..6775e9b 100644 --- a/taoyao-client-android/taoyao/client/src/main/res/values/settings.xml +++ b/taoyao-client-android/taoyao/client/src/main/res/values/settings.xml @@ -34,5 +34,8 @@ true 100 + fd-audio fd-video + 1 + 1 \ No newline at end of file diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/MediaManager.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/MediaManager.java index 5d38e4b..610b2e0 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/MediaManager.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/MediaManager.java @@ -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(); } /** diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/PhotographClient.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/PhotographClient.java index 6f21924..2c9dcb2 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/PhotographClient.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/PhotographClient.java @@ -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; + } } } diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RecordClient.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RecordClient.java index 40db0af..11b4290 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RecordClient.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RecordClient.java @@ -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 audioSamplesQueue; + /** + * 视频队列 + */ private final BlockingQueue 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位最后那个数字是2,8位是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(); } } diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/Room.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/Room.java index 5bc46d4..ccc3d04 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/Room.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/Room.java @@ -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 remoteClients; private final long nativeRoomPointer; private LocalClient localClient; - private Map 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() { diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/SessionClient.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/SessionClient.java index f320a29..1443aed 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/SessionClient.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/SessionClient.java @@ -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 diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/MediaAudioProperties.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/MediaAudioProperties.java index 3beb8b1..6cfea01 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/MediaAudioProperties.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/MediaAudioProperties.java @@ -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; } diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/MediaProperties.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/MediaProperties.java index 37fb72a..5e31c9b 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/MediaProperties.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/MediaProperties.java @@ -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 videos) { this.videos = videos; } + } diff --git a/taoyao-client-media/src/Taoyao.js b/taoyao-client-media/src/Taoyao.js index d074c4d..3ed8457 100644 --- a/taoyao-client-media/src/Taoyao.js +++ b/taoyao-client-media/src/Taoyao.js @@ -1507,7 +1507,7 @@ class Taoyao { interval: 2000, // 范围:-127~0 threshold: -80, - // 采样数量 + // 监控数量 maxEntries: 2, }); // 采样监控 diff --git a/taoyao-client-web/src/components/Config.js b/taoyao-client-web/src/components/Config.js index 473fdbd..b7b38e9 100644 --- a/taoyao-client-web/src/components/Config.js +++ b/taoyao-client-web/src/components/Config.js @@ -8,7 +8,7 @@ const defaultAudioConfig = { volume: 0.5, // 延迟大小(单位毫秒):500毫秒以内较好 latency: 0.4, - // 采样数:8|16|32 + // 采样位数:8|16|32 sampleSize: { min: 8, ideal: 16, max: 32 }, // 采样率:8000|16000|32000|48000 sampleRate: { min: 8000, ideal: 32000, max: 48000 }, diff --git a/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/MediaAudioProperties.java b/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/MediaAudioProperties.java index 0d930a8..82cd16e 100644 --- a/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/MediaAudioProperties.java +++ b/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/MediaAudioProperties.java @@ -7,6 +7,8 @@ import lombok.Setter; /** * 音频配置 * + * 比特率 = 采样率 * 采样位数 * 声道数 / 8 / 1024 + * * @author acgist */ @Getter @@ -30,7 +32,9 @@ public class MediaAudioProperties { @Schema(title = "格式", description = "格式", example = "G722|PCMA|PCMU|OPUS") private Format format; - @Schema(title = "采样数", description = "采样数", example = "8|16|32") + @Schema(title = "比特率", description = "比特率", example = "96|128|256") + private Integer bitrate; + @Schema(title = "采样位数", description = "采样位数", example = "8|16|32") private Integer sampleSize; @Schema(title = "采样率", description = "采样率", example = "8000|16000|32000|48000") private Integer sampleRate; diff --git a/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/MediaProperties.java b/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/MediaProperties.java index 517c1fd..3a18368 100644 --- a/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/MediaProperties.java +++ b/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/MediaProperties.java @@ -27,22 +27,26 @@ public class MediaProperties { private Integer minHeight; @Schema(title = "最大视频高度", description = "最大视频高度") private Integer maxHeight; - @Schema(title = "最小视频码率", description = "最小视频码率") - private Integer minBitrate; - @Schema(title = "最大视频码率", description = "最大视频码率") - private Integer maxBitrate; @Schema(title = "最小视频帧率", description = "最小视频帧率") private Integer minFrameRate; @Schema(title = "最大视频帧率", description = "最大视频帧率") private Integer maxFrameRate; - @Schema(title = "最小音频采样数", description = "最小音频采样数") + @Schema(title = "最小视频码率", description = "最小视频码率") + private Integer minVideoBitrate; + @Schema(title = "最大视频码率", description = "最大视频码率") + private Integer maxVideoBitrate; + @Schema(title = "最小音频采样位数", description = "最小音频采样位数") private Integer minSampleSize; - @Schema(title = "最大音频采样数", description = "最大音频采样数") + @Schema(title = "最大音频采样位数", description = "最大音频采样位数") private Integer maxSampleSize; @Schema(title = "最小音频采样率", description = "最小音频采样率") private Integer minSampleRate; @Schema(title = "最大音频采样率", description = "最大音频采样率") private Integer maxSampleRate; + @Schema(title = "最小音频码率", description = "最小音频码率") + private Integer minAudioBitrate; + @Schema(title = "最大音频码率", description = "最大音频码率") + private Integer maxAudioBitrate; @Schema(title = "音频默认配置", description = "音频默认配置") private MediaAudioProperties audio; @Schema(title = "视频默认配置", description = "视频默认配置") diff --git a/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/MediaVideoProperties.java b/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/MediaVideoProperties.java index efe76ff..3e93a31 100644 --- a/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/MediaVideoProperties.java +++ b/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/MediaVideoProperties.java @@ -6,6 +6,10 @@ import lombok.Setter; /** * 视频配置 + * 原始数据 = 宽 * 高 * 3 / 2 * 8 * 帧率 / 1024 / 1024 + * 视频编码 = 压缩 + * 8 = 颜色位数 + * 3 / 2 = YUV | RGB * * @author acgist */ diff --git a/taoyao-signal-server/taoyao-server/src/main/resources/application.yml b/taoyao-signal-server/taoyao-server/src/main/resources/application.yml index 856cbaf..8ca5334 100644 --- a/taoyao-signal-server/taoyao-server/src/main/resources/application.yml +++ b/taoyao-signal-server/taoyao-server/src/main/resources/application.yml @@ -67,28 +67,34 @@ taoyao: max-client-index: 99999 # 媒体配置 media: + # =============== 视频配置 =============== # 宽度 min-width: 720 max-width: 4096 # 高度 min-height: 480 max-height: 2160 - # 码率 - min-bitrate: 800 - max-bitrate: 1600 # 帧率 min-frame-rate: 15 max-frame-rate: 45 - # 采样数 + # 视频码率 + min-video-bitrate: 800 + max-video-bitrate: 1600 + # =============== 音频配置 =============== + # 采样位数 min-sample-size: 8 max-sample-size: 32 # 采样率 min-sample-rate: 8000 max-sample-rate: 48000 + # 音频码率 + min-audio-bitrate: 96 + max-audio-bitrate: 256 # ABR CBR VBR # 默认音频 audio: format: OPUS + bitrate: 96 sample-size: 32 sample-rate: 44100 # 默认视频 @@ -101,16 +107,19 @@ taoyao: # 超清 fd-audio: format: OPUS + bitrate: 256 sample-size: 32 sample-rate: 48000 # 高清 hd-audio: format: OPUS + bitrate: 128 sample-size: 24 sample-rate: 44100 # 标清 sd-audio: format: OPUS + bitrate: 96 sample-size: 24 sample-rate: 32000 videos: