From 8b83220ccdc0eb64d3e02a1ddff36ebeaf8a583c Mon Sep 17 00:00:00 2001 From: li <719947897@ qq.com> Date: Wed, 29 May 2024 17:56:58 +0800 Subject: [PATCH] ok --- .../android/app/src/main/AndroidManifest.xml | 2 + .../com/zlmediakit/webrtc/MainActivity.kt | 1 + .../zlmediakit/webrtc/PlayerDemoActivity.kt | 56 ++- .../com/zlmediakit/webrtc/PushDemoActivity.kt | 12 - .../zlmediakit/webrtc/PusherDemoActivity.kt | 38 ++ .../src/main/res/layout/activity_player.xml | 121 +++++- .../src/main/res/layout/activity_pusher.xml | 86 +++++ .../android/zlm/src/main/cpp/rtc.cpp | 13 +- .../src/main/java/com/zlm/rtc/NativeLib.kt | 3 +- .../src/main/java/com/zlm/rtc/ZLMRTCPlayer.kt | 16 +- .../src/main/java/com/zlm/rtc/ZLMRTCPush.kt | 45 --- .../src/main/java/com/zlm/rtc/ZLMRTCPusher.kt | 10 + .../zlm/rtc/client/PeerConnectionClient.java | 42 ++- .../com/zlm/rtc/client/VideoFileRecorder.java | 348 ++++++++++++++++++ .../java/com/zlm/rtc/play/ZLMRTCPlayerImpl.kt | 146 ++++---- .../java/com/zlm/rtc/push/ZLMRTCPusherImpl.kt | 205 +++++++++++ 16 files changed, 991 insertions(+), 153 deletions(-) delete mode 100644 webrtc_player/android/app/src/main/java/com/zlmediakit/webrtc/PushDemoActivity.kt create mode 100644 webrtc_player/android/app/src/main/java/com/zlmediakit/webrtc/PusherDemoActivity.kt create mode 100644 webrtc_player/android/app/src/main/res/layout/activity_pusher.xml delete mode 100644 webrtc_player/android/zlm/src/main/java/com/zlm/rtc/ZLMRTCPush.kt create mode 100644 webrtc_player/android/zlm/src/main/java/com/zlm/rtc/ZLMRTCPusher.kt create mode 100644 webrtc_player/android/zlm/src/main/java/com/zlm/rtc/client/VideoFileRecorder.java create mode 100644 webrtc_player/android/zlm/src/main/java/com/zlm/rtc/push/ZLMRTCPusherImpl.kt diff --git a/webrtc_player/android/app/src/main/AndroidManifest.xml b/webrtc_player/android/app/src/main/AndroidManifest.xml index 51814c98..28a013ba 100644 --- a/webrtc_player/android/app/src/main/AndroidManifest.xml +++ b/webrtc_player/android/app/src/main/AndroidManifest.xml @@ -47,6 +47,8 @@ android:screenOrientation="portrait"/> + \ No newline at end of file diff --git a/webrtc_player/android/app/src/main/java/com/zlmediakit/webrtc/MainActivity.kt b/webrtc_player/android/app/src/main/java/com/zlmediakit/webrtc/MainActivity.kt index 18126d70..d22a5b8e 100644 --- a/webrtc_player/android/app/src/main/java/com/zlmediakit/webrtc/MainActivity.kt +++ b/webrtc_player/android/app/src/main/java/com/zlmediakit/webrtc/MainActivity.kt @@ -19,6 +19,7 @@ class MainActivity : AppCompatActivity() { } fun toPushActivity(view: View) { + startActivity(Intent(this, PusherDemoActivity::class.java)) } diff --git a/webrtc_player/android/app/src/main/java/com/zlmediakit/webrtc/PlayerDemoActivity.kt b/webrtc_player/android/app/src/main/java/com/zlmediakit/webrtc/PlayerDemoActivity.kt index 5bd992d3..045b6084 100644 --- a/webrtc_player/android/app/src/main/java/com/zlmediakit/webrtc/PlayerDemoActivity.kt +++ b/webrtc_player/android/app/src/main/java/com/zlmediakit/webrtc/PlayerDemoActivity.kt @@ -1,30 +1,70 @@ package com.zlmediakit.webrtc import android.os.Bundle -import android.os.Handler +import android.view.View +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import com.zlm.rtc.ZLMRTCPlayer +import com.zlm.rtc.play.ZLMRTCPlayerImpl import kotlinx.android.synthetic.main.activity_player.surface_view_renderer +import kotlinx.android.synthetic.main.activity_player.tv_app +import kotlinx.android.synthetic.main.activity_player.tv_stream_id -class PlayerDemoActivity:AppCompatActivity() { +class PlayerDemoActivity : AppCompatActivity() { + + + private val player: ZLMRTCPlayer by lazy { + ZLMRTCPlayerImpl(this) + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_player) - ZLMRTCPlayer.shareInstance().bind(applicationContext,surface_view_renderer,true) + //ffmpeg -re -stream_loop -1 -i "D:\li\hot\data\data\baseline.mp4" -vcodec h264 -acodec aac -f rtsp -rtsp_transport tcp -bf 0 rtsp://zlmediakit.com/live/li + //ffmpeg -re -stream_loop -1 -i "D:\li\hot\data\data\test.mp4" -vcodec h264 -acodec aac -f flv -bf 0 rtmp://zlmediakit.com/live/li - - Handler().postDelayed({ - ZLMRTCPlayer.shareInstance().play("live","li") - },1000) + player.bind(surface_view_renderer, false) } override fun onDestroy() { super.onDestroy() - ZLMRTCPlayer.shareInstance().destroy() + player.stop() + } + + fun onPlayClick(view: View) { + + player.play(tv_app.text.toString(), tv_stream_id.text.toString()) + } + + fun onPauseClick(view: View) { + player.pause() + } + + fun onStopClick(view: View) { + player.stop() + } + + fun onResumeClick(view: View) { + player.resume() + } + + fun onCapture(view: View) { + player.capture { + Toast.makeText(this, "capture ok", Toast.LENGTH_SHORT).show() + } + } + + fun onRecord(view: View) { + player.record(10 * 1000) { + Toast.makeText(this, "" + it, Toast.LENGTH_SHORT).show() + } + } + + fun onVolume(view: View) { + player.setSpeakerphoneOn(true) } } \ No newline at end of file diff --git a/webrtc_player/android/app/src/main/java/com/zlmediakit/webrtc/PushDemoActivity.kt b/webrtc_player/android/app/src/main/java/com/zlmediakit/webrtc/PushDemoActivity.kt deleted file mode 100644 index 4ec9002a..00000000 --- a/webrtc_player/android/app/src/main/java/com/zlmediakit/webrtc/PushDemoActivity.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.zlmediakit.webrtc - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity - -class PushDemoActivity: AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - } -} \ No newline at end of file diff --git a/webrtc_player/android/app/src/main/java/com/zlmediakit/webrtc/PusherDemoActivity.kt b/webrtc_player/android/app/src/main/java/com/zlmediakit/webrtc/PusherDemoActivity.kt new file mode 100644 index 00000000..c5ece879 --- /dev/null +++ b/webrtc_player/android/app/src/main/java/com/zlmediakit/webrtc/PusherDemoActivity.kt @@ -0,0 +1,38 @@ +package com.zlmediakit.webrtc + +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import com.zlm.rtc.ZLMRTCPusher +import com.zlm.rtc.push.ZLMRTCPusherImpl +import kotlinx.android.synthetic.main.activity_player.tv_app +import kotlinx.android.synthetic.main.activity_player.tv_stream_id + +class PusherDemoActivity: AppCompatActivity() { + + + private val pusher: ZLMRTCPusher by lazy { + ZLMRTCPusherImpl(this) + } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_pusher) + + } + + fun onPushCamera(view: View) { + + + + pusher.push(tv_app.text.toString(), tv_stream_id.text.toString()) + } + + + override fun onDestroy() { + super.onDestroy() + pusher.stop() + } +} \ No newline at end of file diff --git a/webrtc_player/android/app/src/main/res/layout/activity_player.xml b/webrtc_player/android/app/src/main/res/layout/activity_player.xml index 88e8f09b..6f9b47b4 100644 --- a/webrtc_player/android/app/src/main/res/layout/activity_player.xml +++ b/webrtc_player/android/app/src/main/res/layout/activity_player.xml @@ -1,13 +1,128 @@ + android:layout_height="match_parent"> + android:layout_height="240dp" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webrtc_player/android/app/src/main/res/layout/activity_pusher.xml b/webrtc_player/android/app/src/main/res/layout/activity_pusher.xml new file mode 100644 index 00000000..9793cba8 --- /dev/null +++ b/webrtc_player/android/app/src/main/res/layout/activity_pusher.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webrtc_player/android/zlm/src/main/cpp/rtc.cpp b/webrtc_player/android/zlm/src/main/cpp/rtc.cpp index c5490091..148306e9 100644 --- a/webrtc_player/android/zlm/src/main/cpp/rtc.cpp +++ b/webrtc_player/android/zlm/src/main/cpp/rtc.cpp @@ -64,10 +64,21 @@ Java_com_zlm_rtc_NativeLib_exchangeSessionDescription(JNIEnv *env, jobject thiz, } extern "C" JNIEXPORT jstring JNICALL -Java_com_zlm_rtc_NativeLib_makeUrl(JNIEnv *env, jobject thiz, jstring app, jstring stream_id) { +Java_com_zlm_rtc_NativeLib_makePlayUrl(JNIEnv *env, jobject thiz, jstring app, jstring stream_id) { const char *appString = env->GetStringUTFChars(app, 0); const char *streamIdString = env->GetStringUTFChars(stream_id, 0); char url[100]; sprintf(url,"https://zlmediakit.com/index/api/webrtc?app=%s&stream=%s&type=play",appString,streamIdString); return env->NewStringUTF(url); +} + + +extern "C" +JNIEXPORT jstring JNICALL +Java_com_zlm_rtc_NativeLib_makePushUrl(JNIEnv *env, jobject thiz, jstring app, jstring stream_id) { + const char *appString = env->GetStringUTFChars(app, 0); + const char *streamIdString = env->GetStringUTFChars(stream_id, 0); + char url[100]; + sprintf(url,"https://zlmediakit.com/index/api/webrtc?app=%s&stream=%s&type=push",appString,streamIdString); + return env->NewStringUTF(url); } \ No newline at end of file diff --git a/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/NativeLib.kt b/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/NativeLib.kt index 142b469a..90112894 100644 --- a/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/NativeLib.kt +++ b/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/NativeLib.kt @@ -10,8 +10,9 @@ class NativeLib { external fun exchangeSessionDescription(description:String): String - external fun makeUrl(app:String,streamId:String): String + external fun makePlayUrl(app:String,streamId:String): String + external fun makePushUrl(app:String,streamId:String): String companion object { // Used to load the 'rtc' library on application startup. diff --git a/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/ZLMRTCPlayer.kt b/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/ZLMRTCPlayer.kt index 145176b4..0ee4fd09 100644 --- a/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/ZLMRTCPlayer.kt +++ b/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/ZLMRTCPlayer.kt @@ -7,11 +7,11 @@ import org.webrtc.SurfaceViewRenderer abstract class ZLMRTCPlayer { - companion object { - fun shareInstance(): ZLMRTCPlayer { - return ZLMRTCPlayerImpl() - } - } +// companion object { +// fun shareInstance(): ZLMRTCPlayer { +// return ZLMRTCPlayerImpl(this) +// } +// } @@ -19,7 +19,7 @@ abstract class ZLMRTCPlayer { constructor() - public abstract fun bind(context: Context,surface: SurfaceViewRenderer, localPreview:Boolean) + public abstract fun bind(surface: SurfaceViewRenderer, localPreview:Boolean) //拉流接口 @@ -35,14 +35,12 @@ abstract class ZLMRTCPlayer { public abstract fun pause() - public abstract fun destroy() - public abstract fun resume() public abstract fun capture(listener: (bitmap: Bitmap) -> Unit) - public abstract fun record(record_duration: Long, result: (path: String) -> Unit) + public abstract fun record(duration: Long, result: (path: String) -> Unit) //推流接口 diff --git a/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/ZLMRTCPush.kt b/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/ZLMRTCPush.kt deleted file mode 100644 index 34f4a1ec..00000000 --- a/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/ZLMRTCPush.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.zlm.rtc - -import android.graphics.Bitmap - -abstract class ZLMRTCPush { - - constructor() - - public abstract fun init(serverUrl: String) - - //拉流接口 - public abstract fun play(app: String, streamId: String) - - public abstract fun setSpeakerphoneOn(on: Boolean) - - public abstract fun setLocalMute(on: Boolean) - - - public abstract fun stop() - - public abstract fun pause() - - public abstract fun resume() - - public abstract fun capture(listener: (bitmap: Bitmap) -> Unit) - - public abstract fun record(record_duration: Long, result: (path: String) -> Unit) - - - - - - //推流接口 -// public abstract fun startLocalPreview() -// -// public abstract fun stopLocalPreview() -// -// public abstract fun startPublishing() -// -// public abstract fun stopPublishing() - - - // - -} \ No newline at end of file diff --git a/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/ZLMRTCPusher.kt b/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/ZLMRTCPusher.kt new file mode 100644 index 00000000..ec38b886 --- /dev/null +++ b/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/ZLMRTCPusher.kt @@ -0,0 +1,10 @@ +package com.zlm.rtc + +import android.graphics.Bitmap + +abstract class ZLMRTCPusher { + abstract fun push(app: String, streamId: String) + + abstract fun stop() + +} \ No newline at end of file diff --git a/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/client/PeerConnectionClient.java b/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/client/PeerConnectionClient.java index 7601e882..f8a0dc46 100644 --- a/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/client/PeerConnectionClient.java +++ b/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/client/PeerConnectionClient.java @@ -190,8 +190,10 @@ public class PeerConnectionClient { private RtcEventLog rtcEventLog; // Implements the WebRtcAudioRecordSamplesReadyCallback interface and writes // recorded audio samples to an output file. - @Nullable - private RecordedAudioToFileController saveRecordedAudioToFile = null; +// @Nullable +// private RecordedAudioToFileController saveRecordedAudioToFile = null; + + private VideoFileRecorder saveVideoFileRecorder = null; /** @@ -489,7 +491,7 @@ public class PeerConnectionClient { WebRtcAudioUtils.setWebRtcBasedNoiseSuppressor(false); } - WebRtcAudioRecord.setOnAudioSamplesReady(saveRecordedAudioToFile); + WebRtcAudioRecord.setOnAudioSamplesReady(saveVideoFileRecorder); // Set audio record error callbacks. WebRtcAudioRecord.setErrorCallback(new WebRtcAudioRecordErrorCallback() { @@ -589,7 +591,7 @@ public class PeerConnectionClient { }; return JavaAudioDeviceModule.builder(appContext) - .setSamplesReadyCallback(saveRecordedAudioToFile) + .setSamplesReadyCallback(saveVideoFileRecorder) .setUseHardwareAcousticEchoCanceler(!peerConnectionParameters.disableBuiltInAEC) .setUseHardwareNoiseSuppressor(!peerConnectionParameters.disableBuiltInNS) .setAudioRecordErrorCallback(audioRecordErrorCallback) @@ -688,10 +690,9 @@ public class PeerConnectionClient { } } - if (saveRecordedAudioToFile != null) { - if (saveRecordedAudioToFile.start()) { - Log.d(TAG, "Recording input audio to file is activated"); - } + if (saveVideoFileRecorder == null) { + saveVideoFileRecorder = new VideoFileRecorder(); + } Log.d(TAG, "Peer connection created."); @@ -872,6 +873,23 @@ public class PeerConnectionClient { }); } + public void setRecordEnable(final boolean enable, String savePath) { + executor.execute(() -> { + if (saveVideoFileRecorder != null) { + if (enable) { + try { + saveVideoFileRecorder.start(savePath, rootEglBase.getEglBaseContext(), false); + } catch (IOException e) { + //throw new RuntimeException(e); + } + } else { + saveVideoFileRecorder.release(); + } + + } + }); + } + public void createOffer(final BigInteger handleId) { Log.d(TAG, "peerConnectionMap get handleId=" + peerConnectionMap.size()); executor.execute(() -> { @@ -1362,7 +1380,11 @@ public class PeerConnectionClient { remoteVideoTrack.setEnabled(true); connection.videoTrack = remoteVideoTrack; connection.videoTrack.addSink(videoSinkMap.get(connection.handleId)); + if (saveVideoFileRecorder != null) { + connection.videoTrack.addSink(saveVideoFileRecorder); + } events.onRemoteRender(connection.handleId); + } } }); @@ -1453,10 +1475,6 @@ public class PeerConnectionClient { if (peerConnection != null && !isError) { Log.d(TAG, "Set local SDP from " + sdp.type); peerConnection.setLocalDescription(sdpObserver, sdp); - -// MediaStream localMediaStream = factory.createLocalMediaStream("ARDAMS"); -// localMediaStream.addTrack(localAudioTrack); -// peerConnection.addStream(localMediaStream); } }); } diff --git a/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/client/VideoFileRecorder.java b/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/client/VideoFileRecorder.java new file mode 100644 index 00000000..87efdf8a --- /dev/null +++ b/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/client/VideoFileRecorder.java @@ -0,0 +1,348 @@ +package com.zlm.rtc.client; + +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.media.MediaMuxer; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; +import android.view.Surface; + +import org.webrtc.EglBase; +import org.webrtc.GlRectDrawer; +import org.webrtc.VideoFrame; +import org.webrtc.VideoFrameDrawer; +import org.webrtc.VideoSink; +import org.webrtc.audio.JavaAudioDeviceModule; +import org.webrtc.voiceengine.WebRtcAudioRecord; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * @author leo + * @version 1.0 + * @className VideoFileRenderer + * @description TODO + * @date 2022/9/27 11:12 + **/ + +class VideoFileRecorder implements VideoSink, JavaAudioDeviceModule.SamplesReadyCallback, WebRtcAudioRecord.WebRtcAudioRecordSamplesReadyCallback { + private static final String TAG = "VideoFileRenderer"; + + + private String mOutFilePath; + private HandlerThread renderThread; + private Handler renderThreadHandler; + private HandlerThread audioThread; + private Handler audioThreadHandler; + private int outputFileWidth = -1; + private int outputFileHeight = -1; + private ByteBuffer[] encoderOutputBuffers; + private ByteBuffer[] audioInputBuffers; + private ByteBuffer[] audioOutputBuffers; + private EglBase eglBase; + private EglBase.Context sharedContext; + private VideoFrameDrawer frameDrawer; + + // TODO: these ought to be configurable as well + private static final String MIME_TYPE = "video/avc"; // H.264 Advanced Video Coding + private static final int FRAME_RATE = 15; // 30fps + private static final int IFRAME_INTERVAL = 5; // 5 seconds between I-frames + + private MediaMuxer mediaMuxer; + private MediaCodec encoder; + private MediaCodec.BufferInfo bufferInfo, audioBufferInfo; + private int trackIndex = -1; + private int audioTrackIndex; + private boolean withAudio = false; + private boolean isEnableRecord = false; + + private GlRectDrawer drawer; + private Surface surface; + private MediaCodec audioEncoder; + + VideoFileRecorder() { + Log.i(TAG, "=====================>VideoFileRecorder"); + renderThread = new HandlerThread(TAG + "RenderThread"); + renderThread.start(); + renderThreadHandler = new Handler(renderThread.getLooper()); + isEnableRecord = false; + } + + + public void start(String outputFile, final EglBase.Context sharedContext, boolean withAudio) throws IOException { + Log.i(TAG, "=====================>start"); + isEnableRecord = true; + trackIndex = -1; + outputFileWidth = -1; + this.sharedContext = sharedContext; + this.withAudio = withAudio; + + if (this.withAudio) { + audioThread = new HandlerThread(TAG + "AudioThread"); + audioThread.start(); + audioThreadHandler = new Handler(audioThread.getLooper()); + } else { + audioThread = null; + audioThreadHandler = null; + } + bufferInfo = new MediaCodec.BufferInfo(); + this.mOutFilePath = outputFile; + mediaMuxer = new MediaMuxer(outputFile, + MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); + audioTrackIndex = this.withAudio ? -1 : 0; + } + + + /** + * Release all resources. All already posted frames will be rendered first. + */ + public void release() { + isEnableRecord = false; + if (audioThreadHandler != null) { + audioThreadHandler.post(() -> { + if (audioEncoder != null) { + audioEncoder.stop(); + audioEncoder.release(); + } + audioThread.quit(); + }); + } + + if (renderThreadHandler != null) { + renderThreadHandler.post(() -> { + if (encoder != null) { + encoder.stop(); + encoder.release(); + } + eglBase.release(); + mediaMuxer.stop(); + mediaMuxer.release(); + renderThread.quit(); + + }); + } + } + + public boolean isRecording() { + return isEnableRecord; + } + + private void initVideoEncoder() { + MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, outputFileWidth, outputFileHeight); + + // Set some properties. Failing to specify some of these can cause the MediaCodec + // configure() call to throw an unhelpful exception. + format.setInteger(MediaFormat.KEY_COLOR_FORMAT, + MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + format.setInteger(MediaFormat.KEY_BIT_RATE, 6000000); + format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); + format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); + + // Create a MediaCodec encoder, and configure it with our format. Get a Surface + // we can use for input and wrap it with a class that handles the EGL work. + try { + encoder = MediaCodec.createEncoderByType(MIME_TYPE); + encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + renderThreadHandler.post(() -> { + eglBase = EglBase.create(sharedContext, EglBase.CONFIG_RECORDABLE); + surface = encoder.createInputSurface(); + eglBase.createSurface(surface); + eglBase.makeCurrent(); + drawer = new GlRectDrawer(); + }); + } catch (Exception e) { + Log.wtf(TAG, e); + } + } + + @Override + public void onFrame(VideoFrame frame) { + if (!isEnableRecord) return; + Log.e(TAG, "onFrame"); + frame.retain(); + if (outputFileWidth == -1) { + outputFileWidth = frame.getRotatedWidth(); + outputFileHeight = frame.getRotatedHeight(); + initVideoEncoder(); + } + renderThreadHandler.post(() -> renderFrameOnRenderThread(frame)); + } + + private void renderFrameOnRenderThread(VideoFrame frame) { + if (frameDrawer == null) { + frameDrawer = new VideoFrameDrawer(); + } + frameDrawer.drawFrame(frame, drawer, null, 0, 0, outputFileWidth, outputFileHeight); + frame.release(); + drainEncoder(); + eglBase.swapBuffers(); + } + + + private boolean encoderStarted = false; + private volatile boolean muxerStarted = false; + private long videoFrameStart = 0; + + private void drainEncoder() { + if (!encoderStarted) { + encoder.start(); + encoderOutputBuffers = encoder.getOutputBuffers(); + encoderStarted = true; + return; + } + while (true) { + try { + int encoderStatus = encoder.dequeueOutputBuffer(bufferInfo, 10000); + if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { + break; + } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + // not expected for an encoder + encoderOutputBuffers = encoder.getOutputBuffers(); + Log.e(TAG, "encoder output buffers changed"); + } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + // not expected for an encoder + MediaFormat newFormat = encoder.getOutputFormat(); + + Log.e(TAG, "encoder output format changed: " + newFormat); + trackIndex = mediaMuxer.addTrack(newFormat); + if (audioTrackIndex != -1 && !muxerStarted) { + mediaMuxer.start(); + Log.e(TAG, "mediaMuxer start"); + muxerStarted = true; + } + if (!muxerStarted) + break; + } else if (encoderStatus < 0) { + Log.e(TAG, "unexpected result fr om encoder.dequeueOutputBuffer: " + encoderStatus); + } else { // encoderStatus >= 0 + try { + ByteBuffer encodedData = encoderOutputBuffers[encoderStatus]; + if (encodedData == null) { + Log.e(TAG, "encoderOutputBuffer " + encoderStatus + " was null"); + break; + } + // It's usually necessary to adjust the ByteBuffer values to match BufferInfo. + encodedData.position(bufferInfo.offset); + encodedData.limit(bufferInfo.offset + bufferInfo.size); + if (videoFrameStart == 0 && bufferInfo.presentationTimeUs != 0) { + videoFrameStart = bufferInfo.presentationTimeUs; + } + bufferInfo.presentationTimeUs -= videoFrameStart; + if (muxerStarted) + mediaMuxer.writeSampleData(trackIndex, encodedData, bufferInfo); + isEnableRecord = isEnableRecord && (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == 0; + encoder.releaseOutputBuffer(encoderStatus, false); + if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + break; + } + } catch (Exception e) { + Log.wtf(TAG, e); + break; + } + } + } catch (Exception e) { + Log.e(TAG, "encoder error, " + e); + break; + } + } + } + + private long presTime = 0L; + + private void drainAudio() { + if (audioBufferInfo == null) + audioBufferInfo = new MediaCodec.BufferInfo(); + while (true) { + int encoderStatus = audioEncoder.dequeueOutputBuffer(audioBufferInfo, 10000); + if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { + break; + } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + // not expected for an encoder + audioOutputBuffers = audioEncoder.getOutputBuffers(); + Log.w(TAG, "encoder output buffers changed"); + } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + // not expected for an encoder + MediaFormat newFormat = audioEncoder.getOutputFormat(); + + Log.w(TAG, "encoder output format changed: " + newFormat); + audioTrackIndex = mediaMuxer.addTrack(newFormat); + if (trackIndex != -1 && !muxerStarted) { + mediaMuxer.start(); + muxerStarted = true; + } + if (!muxerStarted) + break; + } else if (encoderStatus < 0) { + Log.e(TAG, "unexpected result fr om encoder.dequeueOutputBuffer: " + encoderStatus); + } else { // encoderStatus >= 0 + try { + ByteBuffer encodedData = audioOutputBuffers[encoderStatus]; + if (encodedData == null) { + Log.e(TAG, "encoderOutputBuffer " + encoderStatus + " was null"); + break; + } + // It's usually necessary to adjust the ByteBuffer values to match BufferInfo. + encodedData.position(audioBufferInfo.offset); + encodedData.limit(audioBufferInfo.offset + audioBufferInfo.size); + if (muxerStarted) + mediaMuxer.writeSampleData(audioTrackIndex, encodedData, audioBufferInfo); + isEnableRecord = isEnableRecord && (audioBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == 0; + audioEncoder.releaseOutputBuffer(encoderStatus, false); + if ((audioBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + break; + } + } catch (Exception e) { + Log.wtf(TAG, e); + break; + } + } + } + } + + @Override + public void onWebRtcAudioRecordSamplesReady(JavaAudioDeviceModule.AudioSamples audioSamples) { + if (!isEnableRecord) return; + Log.e(TAG, "onWebRtcAudioRecordSamplesReady " + isEnableRecord); + if (!isEnableRecord) + return; + if (audioThreadHandler != null) { + audioThreadHandler.post(() -> { + if (audioEncoder == null) try { + audioEncoder = MediaCodec.createEncoderByType("audio/mp4a-latm"); + MediaFormat format = new MediaFormat(); + format.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm"); + format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, audioSamples.getChannelCount()); + format.setInteger(MediaFormat.KEY_SAMPLE_RATE, audioSamples.getSampleRate()); + format.setInteger(MediaFormat.KEY_BIT_RATE, 64 * 1024); + format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); + audioEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + audioEncoder.start(); + audioInputBuffers = audioEncoder.getInputBuffers(); + audioOutputBuffers = audioEncoder.getOutputBuffers(); + } catch (IOException exception) { + Log.wtf(TAG, exception); + } + int bufferIndex = audioEncoder.dequeueInputBuffer(0); + if (bufferIndex >= 0) { + ByteBuffer buffer = audioInputBuffers[bufferIndex]; + buffer.clear(); + byte[] data = audioSamples.getData(); + buffer.put(data); + audioEncoder.queueInputBuffer(bufferIndex, 0, data.length, presTime, 0); + presTime += data.length * 125 / 12; // 1000000 microseconds / 48000hz / 2 bytes + } + drainAudio(); + }); + } + + } + + @Override + public void onWebRtcAudioRecordSamplesReady(WebRtcAudioRecord.AudioSamples samples) { + onWebRtcAudioRecordSamplesReady(new JavaAudioDeviceModule.AudioSamples(samples.getAudioFormat(), + samples.getChannelCount(), samples.getSampleRate(), samples.getData())); + } +} \ No newline at end of file diff --git a/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/play/ZLMRTCPlayerImpl.kt b/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/play/ZLMRTCPlayerImpl.kt index 8351789d..4483584b 100644 --- a/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/play/ZLMRTCPlayerImpl.kt +++ b/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/play/ZLMRTCPlayerImpl.kt @@ -2,6 +2,8 @@ package com.zlm.rtc.play import android.content.Context import android.graphics.Bitmap +import android.media.AudioManager +import android.os.Handler import android.util.Log import com.zlm.rtc.NativeLib import com.zlm.rtc.ZLMRTCPlayer @@ -18,56 +20,36 @@ import org.webrtc.SessionDescription import org.webrtc.StatsReport import org.webrtc.SurfaceViewRenderer import org.webrtc.VideoCapturer +import java.io.File import java.math.BigInteger +import kotlin.random.Random -class ZLMRTCPlayerImpl : ZLMRTCPlayer(), PeerConnectionClient.PeerConnectionEvents { - - private var context: Context? = null +class ZLMRTCPlayerImpl(val context: Context) : ZLMRTCPlayer(), + PeerConnectionClient.PeerConnectionEvents { private var surfaceViewRenderer: SurfaceViewRenderer? = null - private val eglBase = EglBase.create() + private var eglBase: EglBase? = null - private val peerConnectionClient: PeerConnectionClient? by lazy { + private var defaultFps = 24 - PeerConnectionClient( - context, eglBase, - PeerConnectionClient.PeerConnectionParameters( - false, - true, - false, - 1280, - 720, - 15, - 0, - "H264", - true, - true, - 0, - "OPUS", - false, - false, - false, - false, - false, - false, - false, - false, false, false, null - ), this - ) - } + private var peerConnectionClient: PeerConnectionClient? = null - init { + private var localHandleId = BigInteger.valueOf(Random(1024).nextLong()) + + private var audioManager: AudioManager? = null + + private var app: String = "" + private var streamId: String = "" - } private fun logger(msg: String) { Log.i("ZLMRTCPlayerImpl", msg) } - fun createVideoCapture(context: Context?): VideoCapturer? { + private fun createVideoCapture(context: Context?): VideoCapturer? { val videoCapturer: VideoCapturer? = if (Camera2Enumerator.isSupported(context)) { createCameraCapture(Camera2Enumerator(context)) } else { @@ -105,72 +87,108 @@ class ZLMRTCPlayerImpl : ZLMRTCPlayer(), PeerConnectionClient.PeerConnectionEven return null } - override fun bind(context: Context, surface: SurfaceViewRenderer, localPreview: Boolean) { - this.context = context + private fun initPeerConnectionClient(): PeerConnectionClient { + eglBase = EglBase.create() + return PeerConnectionClient( + context, eglBase, + PeerConnectionClient.PeerConnectionParameters( + false, + false, + false, + 1280, + 720, + defaultFps, + 1024 * 1000 * 2, + "H264", + true, + true, + 0, + "OPUS", + false, + false, + false, + false, + false, + false, + false, + false, false, false, null + ), this + ) + } + + override fun bind(surface: SurfaceViewRenderer, localPreview: Boolean) { this.surfaceViewRenderer = surface - this.surfaceViewRenderer?.init(eglBase.eglBaseContext,null) - this.peerConnectionClient?.setAudioEnabled(true) - peerConnectionClient?.createPeerConnectionFactory(PeerConnectionFactory.Options()) - peerConnectionClient?.createPeerConnection(createVideoCapture(context), BigInteger.ONE) - peerConnectionClient?.createOffer((BigInteger.ONE)) - - + audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + audioManager?.isSpeakerphoneOn = false } override fun play(app: String, streamId: String) { + this.app = app + this.streamId = streamId + if (peerConnectionClient == null) peerConnectionClient = initPeerConnectionClient() + surfaceViewRenderer?.init(eglBase?.eglBaseContext, null) + peerConnectionClient?.setAudioEnabled(true) + peerConnectionClient?.createPeerConnectionFactory(PeerConnectionFactory.Options()) + peerConnectionClient?.createPeerConnection(createVideoCapture(context), localHandleId) + peerConnectionClient?.createOffer(localHandleId) } override fun setSpeakerphoneOn(on: Boolean) { - + audioManager?.isSpeakerphoneOn = on } override fun setLocalMute(on: Boolean) { - + audioManager?.isSpeakerphoneOn = on } override fun stop() { - + surfaceViewRenderer?.clearImage() + surfaceViewRenderer?.release() + peerConnectionClient?.stopVideoSource() + peerConnectionClient?.close() + peerConnectionClient = null } override fun pause() { - + surfaceViewRenderer?.pauseVideo() } - override fun destroy() { - - peerConnectionClient?.close() - - } override fun resume() { + surfaceViewRenderer?.setFpsReduction(defaultFps.toFloat()) } override fun capture(listener: (bitmap: Bitmap) -> Unit) { - + surfaceViewRenderer?.addFrameListener({ + listener.invoke(it) + }, 1f) } - override fun record(record_duration: Long, result: (path: String) -> Unit) { + override fun record(duration: Long, result: (path: String) -> Unit) { + val savePath = context.cacheDir.absoluteFile.absolutePath + File.separator + System.currentTimeMillis() + ".mp4" + peerConnectionClient?.setRecordEnable(true,savePath) + Handler().postDelayed({ + peerConnectionClient?.setRecordEnable(false, savePath) + }, duration) } override fun onLocalDescription(handleId: BigInteger?, sdp: SessionDescription?) { - val url = NativeLib().makeUrl("live", "li") - logger("handleId: " + url) + val url = NativeLib().makePlayUrl(app, streamId) + logger("handleId: $url") logger("handleId: " + sdp?.description) val doPost = HttpClient.doPost( url, mutableMapOf(Pair("sdp", sdp?.description)), mutableMapOf() ) - val result = JSONObject(doPost) - val code = result.getInt("code") if (code == 0) { - logger("handleId: " + doPost) + logger("handleId: $doPost") val sdp = result.getString("sdp") peerConnectionClient?.setRemoteDescription( handleId, @@ -178,7 +196,7 @@ class ZLMRTCPlayerImpl : ZLMRTCPlayer(), PeerConnectionClient.PeerConnectionEven ) } else { val msg = result.getString("msg") - logger("handleId: " + msg) + logger("handleId: $msg") } } @@ -219,12 +237,16 @@ class ZLMRTCPlayerImpl : ZLMRTCPlayer(), PeerConnectionClient.PeerConnectionEven override fun onLocalRender(handleId: BigInteger?) { logger("onLocalRender: " + handleId) //peerConnectionClient?.setVideoRender(handleId, surfaceViewRenderer) +// if (handleId == localHandleId) { +// peerConnectionClient?.setVideoRender(handleId, surfaceViewRenderer) +// } } override fun onRemoteRender(handleId: BigInteger?) { logger("onRemoteRender: " + handleId) - peerConnectionClient?.setVideoRender(handleId, surfaceViewRenderer) - + if (handleId == localHandleId) { + peerConnectionClient?.setVideoRender(handleId, surfaceViewRenderer) + } } diff --git a/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/push/ZLMRTCPusherImpl.kt b/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/push/ZLMRTCPusherImpl.kt new file mode 100644 index 00000000..8ccaed9f --- /dev/null +++ b/webrtc_player/android/zlm/src/main/java/com/zlm/rtc/push/ZLMRTCPusherImpl.kt @@ -0,0 +1,205 @@ +package com.zlm.rtc.push + +import android.content.Context +import android.graphics.Bitmap +import android.media.AudioManager +import android.util.Log +import com.zlm.rtc.NativeLib +import com.zlm.rtc.ZLMRTCPusher +import com.zlm.rtc.client.HttpClient +import com.zlm.rtc.client.PeerConnectionClient +import org.json.JSONObject +import org.webrtc.Camera1Enumerator +import org.webrtc.Camera2Enumerator +import org.webrtc.CameraEnumerator +import org.webrtc.EglBase +import org.webrtc.IceCandidate +import org.webrtc.PeerConnectionFactory +import org.webrtc.SessionDescription +import org.webrtc.StatsReport +import org.webrtc.SurfaceViewRenderer +import org.webrtc.VideoCapturer +import java.math.BigInteger +import kotlin.random.Random + +class ZLMRTCPusherImpl(val context:Context) :ZLMRTCPusher(), + PeerConnectionClient.PeerConnectionEvents { + + + private var peerConnectionClient: PeerConnectionClient? = null + + private var eglBase: EglBase? = null + + private var defaultFps = 24 + + private var surfaceViewRenderer: SurfaceViewRenderer? = null + + private var localHandleId = BigInteger.valueOf(Random(2048).nextLong()) + + private var app: String = "" + private var streamId: String = "" + + private fun initPeerConnectionClient(): PeerConnectionClient { + eglBase = EglBase.create() + return PeerConnectionClient( + context, eglBase, + PeerConnectionClient.PeerConnectionParameters( + true, + false, + false, + 1280, + 720, + defaultFps, + 1024 * 1000 * 2, + "H264", + true, + true, + 0, + "OPUS", + false, + false, + false, + false, + false, + false, + false, + false, false, false, null + ), this + ) + } + + private fun createVideoCapture(context: Context?): VideoCapturer? { + val videoCapturer: VideoCapturer? = if (Camera2Enumerator.isSupported(context)) { + createCameraCapture(Camera2Enumerator(context)) + } else { + createCameraCapture(Camera1Enumerator(true)) + } + return videoCapturer + } + + + /** + * 创建相机媒体流 + */ + private fun createCameraCapture(enumerator: CameraEnumerator): VideoCapturer? { + val deviceNames = enumerator.deviceNames + + // Front facing camera not found, try something else + for (deviceName in deviceNames) { + if (enumerator.isFrontFacing(deviceName)) { + val videoCapturer: VideoCapturer? = enumerator.createCapturer(deviceName, null) + if (videoCapturer != null) { + return videoCapturer + } + } + } + // First, try to find front facing camera + for (deviceName in deviceNames) { + if (enumerator.isFrontFacing(deviceName)) { + val videoCapturer: VideoCapturer? = enumerator.createCapturer(deviceName, null) + if (videoCapturer != null) { + return videoCapturer + } + } + } + + + return null + } + + private fun logger(msg: String) { + Log.i("ZLMRTCPusherImpl", msg) + } + + + + override fun push(app: String, streamId: String) { + this.app = app + this.streamId = streamId + if (peerConnectionClient == null) peerConnectionClient = initPeerConnectionClient() + surfaceViewRenderer?.init(eglBase?.eglBaseContext, null) + peerConnectionClient?.setAudioEnabled(true) + peerConnectionClient?.setVideoEnabled(true) + peerConnectionClient?.createPeerConnectionFactory(PeerConnectionFactory.Options()) + peerConnectionClient?.createPeerConnection(createVideoCapture(context), localHandleId) + peerConnectionClient?.createOffer(localHandleId) + } + + + override fun stop() { + surfaceViewRenderer?.clearImage() + surfaceViewRenderer?.release() + peerConnectionClient?.stopVideoSource() + peerConnectionClient?.close() + peerConnectionClient = null + } + + override fun onLocalDescription(handleId: BigInteger?, sdp: SessionDescription?) { + val url = NativeLib().makePushUrl(app, streamId) + logger("handleId: $url") + logger("handleId: " + sdp?.description) + val doPost = HttpClient.doPost( + url, + mutableMapOf(Pair("sdp", sdp?.description)), + mutableMapOf() + ) + val result = JSONObject(doPost) + val code = result.getInt("code") + if (code == 0) { + logger("handleId: $doPost") + val sdp = result.getString("sdp") + peerConnectionClient?.setRemoteDescription( + handleId, + SessionDescription(SessionDescription.Type.ANSWER, sdp) + ) + } else { + val msg = result.getString("msg") + logger("handleId: $msg") + } + } + + override fun onIceCandidate(handleId: BigInteger?, candidate: IceCandidate?) { + + } + + override fun onIceCandidatesRemoved( + handleId: BigInteger?, + candidates: Array? + ) { + + } + + override fun onIceConnected(handleId: BigInteger?) { + + } + + override fun onIceDisconnected(handleId: BigInteger?) { + + } + + override fun onPeerConnectionClosed(handleId: BigInteger?) { + + } + + override fun onPeerConnectionStatsReady( + handleId: BigInteger?, + reports: Array? + ) { + + } + + override fun onPeerConnectionError(handleId: BigInteger?, description: String?) { + + } + + override fun onLocalRender(handleId: BigInteger?) { + if (handleId == localHandleId) { + peerConnectionClient?.setVideoRender(handleId, surfaceViewRenderer) + } + } + + override fun onRemoteRender(handleId: BigInteger?) { + + } + +} \ No newline at end of file