From ac4e264950c73244c3f7ca27f6062204a95e5a14 Mon Sep 17 00:00:00 2001 From: acgist <289547414@qq.com> Date: Wed, 12 Apr 2023 08:24:34 +0800 Subject: [PATCH] [*] jni --- .../acgist/taoyao/boot/utils/ListUtils.java | 37 ++ .../acgist/taoyao/client/MainActivity.java | 11 +- .../acgist/taoyao/client/MediaService.java | 5 +- .../acgist/taoyao/client/signal/Taoyao.java | 25 +- .../media/src/main/cpp/include/Room.hpp | 26 +- .../src/main/cpp/include/RouterCallback.hpp | 26 +- .../taoyao/media/src/main/cpp/webrtc/Room.cpp | 172 ++++-- .../src/main/cpp/webrtc/RouterCallback.cpp | 139 +++-- .../com/acgist/taoyao/media/MediaManager.java | 573 +++++++++++------- .../acgist/taoyao/media/RouterCallback.java | 14 +- .../acgist/taoyao/media/VideoSourceType.java | 34 ++ .../taoyao/media/client/LocalClient.java | 49 +- .../taoyao/media/client/PhotographClient.java | 56 ++ .../taoyao/media/client/RecordClient.java | 18 +- .../taoyao/media/client/RemoteClient.java | 50 +- .../com/acgist/taoyao/media/client/Room.java | 61 +- .../taoyao/media/client/SessionClient.java | 59 +- .../acgist/taoyao/media/config/Config.java | 4 + .../taoyao/media/config/WebrtcProperties.java | 41 ++ .../media/config/WebrtcStunProperties.java | 33 + .../media/config/WebrtcTurnProperties.java | 33 + taoyao-client-web/src/components/Taoyao.js | 3 - 22 files changed, 1034 insertions(+), 435 deletions(-) create mode 100644 taoyao-client-android/taoyao/boot/src/main/java/com/acgist/taoyao/boot/utils/ListUtils.java create mode 100644 taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/VideoSourceType.java create mode 100644 taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/PhotographClient.java create mode 100644 taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/WebrtcProperties.java create mode 100644 taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/WebrtcStunProperties.java create mode 100644 taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/WebrtcTurnProperties.java diff --git a/taoyao-client-android/taoyao/boot/src/main/java/com/acgist/taoyao/boot/utils/ListUtils.java b/taoyao-client-android/taoyao/boot/src/main/java/com/acgist/taoyao/boot/utils/ListUtils.java new file mode 100644 index 0000000..cbcaf68 --- /dev/null +++ b/taoyao-client-android/taoyao/boot/src/main/java/com/acgist/taoyao/boot/utils/ListUtils.java @@ -0,0 +1,37 @@ +package com.acgist.taoyao.boot.utils; + +import android.util.Log; + +import java.util.List; +import java.util.function.Function; + +/** + * 集合工具 + * + * @author acgist + */ +public final class ListUtils { + + private ListUtils() { + } + + /** + * @param list 集合 + * @param function 执行函数 + * + * @return 集合首个元素 + * + * @param 集合类型 + */ + public static final T getOnlyOne(List list, Function function) { + if(list == null || list.isEmpty()) { + return null; + } + final int size = list.size(); + if(size > 1) { + Log.w(ListUtils.class.getSimpleName(), "集合不止一条数据:" + size); + } + return function.apply(list.get(0)); + } + +} 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 4d1f016..05d74df 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 @@ -153,6 +153,8 @@ public class MainActivity extends AppCompatActivity implements Serializable { resources.getBoolean(R.bool.videoConsume), resources.getBoolean(R.bool.audioProduce), resources.getBoolean(R.bool.videoProduce), + resources.getString(R.string.storagePathImage), + resources.getString(R.string.storagePathVideo), TransportType.valueOf(resources.getString(R.string.transportType)) ); final Display display = this.getWindow().getContext().getDisplay(); @@ -209,19 +211,16 @@ public class MainActivity extends AppCompatActivity implements Serializable { private void action(View view) { this.threadHandler.post(() -> { // 进入房间 - Taoyao.taoyao.roomEnter("022a16bd-94b9-4d40-889e-d364da065bd2", null); + Taoyao.taoyao.roomEnter("1e6707a5-6846-405e-95de-632aa01569aa", null); }); } private void switchRecord(View view) { final MediaManager mediaManager = MediaManager.getInstance(); if (mediaManager.isRecording()) { - mediaManager.stopRecord(); + mediaManager.stopRecordVideoCapture(); } else { - mediaManager.startRecord( - this.getResources().getString(R.string.storagePathVideo), - DateUtils.format(LocalDateTime.now(), DateUtils.DateTimeStyle.YYYYMMDDHH24MMSS) + ".mp4" - ); + mediaManager.startRecordVideoCapture(); } } diff --git a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MediaService.java b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MediaService.java index a1a231a..3bb38b0 100644 --- a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MediaService.java +++ b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MediaService.java @@ -18,13 +18,10 @@ import android.widget.Toast; import androidx.core.app.NotificationCompat; -import com.acgist.taoyao.boot.model.Message; import com.acgist.taoyao.client.signal.Taoyao; import com.acgist.taoyao.media.MediaManager; import com.acgist.taoyao.media.signal.ITaoyaoListener; -import java.util.Map; - /** * 媒体服务 * @@ -191,7 +188,7 @@ public class MediaService extends Service { final NotificationManager notificationManager = this.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); this.startForeground((int) System.currentTimeMillis(), notification); - MediaManager.getInstance().screenRecord(intent.getParcelableExtra("data")); + MediaManager.getInstance().initScreen(intent.getParcelableExtra("data")); } } \ No newline at end of file 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 f62184b..116bae3 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 @@ -25,10 +25,13 @@ import com.acgist.taoyao.boot.utils.IdUtils; import com.acgist.taoyao.boot.utils.JSONUtils; import com.acgist.taoyao.boot.utils.MapUtils; import com.acgist.taoyao.client.R; +import com.acgist.taoyao.media.config.MediaAudioProperties; import com.acgist.taoyao.media.config.MediaProperties; import com.acgist.taoyao.media.MediaManager; import com.acgist.taoyao.media.client.Room; import com.acgist.taoyao.media.client.SessionClient; +import com.acgist.taoyao.media.config.MediaVideoProperties; +import com.acgist.taoyao.media.config.WebrtcProperties; import com.acgist.taoyao.media.signal.ITaoyao; import com.acgist.taoyao.media.signal.ITaoyaoListener; @@ -162,10 +165,6 @@ public final class Taoyao implements ITaoyao { private final Handler executeMessageHandler; private final HandlerThread executeMessageThread; private final MediaManager mediaManager; - /** - * 媒体配置 - */ - private MediaProperties mediaProperties; /** * 房间列表 */ @@ -511,6 +510,8 @@ public final class Taoyao implements ITaoyao { return; } switch (header.getSignal()) { + case "control::config::audio" -> this.controlConfigAudio(message, message.body()); + case "control::config::video" -> this.controlConfigVideo(message, message.body()); case "client::config" -> this.clientConfig(message, message.body()); case "client::register" -> this.clientRegister(message, message.body()); case "client::reboot" -> this.clientReboot(message, message.body()); @@ -563,12 +564,26 @@ public final class Taoyao implements ITaoyao { )); } + private void controlConfigAudio(Message message, Map body) { + final MediaAudioProperties mediaAudioProperties = JSONUtils.toJava(JSONUtils.toJSON(body), MediaAudioProperties.class); + this.mediaManager.updateAudioConfig(mediaAudioProperties); + + } + + private void controlConfigVideo(Message message, Map body) { + final MediaVideoProperties mediaVideoProperties = JSONUtils.toJava(JSONUtils.toJSON(body), MediaVideoProperties.class); + this.mediaManager.updateVideoConfig(mediaVideoProperties); + } + /** * @param message 消息 * @param body 消息主体 */ private void clientConfig(Message message, Map body) { - this.mediaProperties = JSONUtils.toJava(JSONUtils.toJSON(body), MediaProperties.class); + final MediaProperties mediaProperties = JSONUtils.toJava(JSONUtils.toJSON(body.get("media")), MediaProperties.class); + this.mediaManager.updateMediaConfig(mediaProperties, mediaProperties.getAudio(), mediaProperties.getVideo()); + final WebrtcProperties webrtcProperties = JSONUtils.toJava(JSONUtils.toJSON(body.get("webrtc")), WebrtcProperties.class); + this.mediaManager.updateWebrtcConfig(webrtcProperties); } /** diff --git a/taoyao-client-android/taoyao/media/src/main/cpp/include/Room.hpp b/taoyao-client-android/taoyao/media/src/main/cpp/include/Room.hpp index 7dfe265..4d55837 100644 --- a/taoyao-client-android/taoyao/media/src/main/cpp/include/Room.hpp +++ b/taoyao-client-android/taoyao/media/src/main/cpp/include/Room.hpp @@ -26,21 +26,21 @@ namespace acgist { mediasoupclient::Consumer::Listener* consumerListener; std::map consumers; public: - Room(std::string roomId, JNIEnv* env, jobject routerCallback); + Room(std::string roomId, JavaVM* javaVM, jobject routerCallback); virtual ~Room(); public: - void enter(std::string rtpCapabilities, webrtc::PeerConnectionFactoryInterface* factory, webrtc::PeerConnectionInterface::RTCConfiguration& rtcConfiguration); - void createSendTransport(std::string body); - void createRecvTransport(std::string body); - void mediaProduceAudio(webrtc::MediaStreamInterface* mediaStream); - void mediaProduceVideo(webrtc::MediaStreamInterface* mediaStream); - void mediaConsume(std::string message); - void mediaProducerPause(std::string producerId); - void mediaProducerResume(std::string producerId); - void mediaProducerClose(std::string producerId); - void mediaConsumerPause(std::string consumerId); - void mediaConsumerResume(std::string consumerId); - void mediaConsumerClose(std::string consumerId); + void enter(JNIEnv* env, std::string rtpCapabilities, webrtc::PeerConnectionFactoryInterface* factory, webrtc::PeerConnectionInterface::RTCConfiguration& rtcConfiguration); + void createSendTransport(JNIEnv* env, std::string body); + void createRecvTransport(JNIEnv* env, std::string body); + void mediaProduceAudio(JNIEnv* env, webrtc::MediaStreamInterface* mediaStream); + void mediaProduceVideo(JNIEnv* env, webrtc::MediaStreamInterface* mediaStream); + void mediaConsume(JNIEnv* env, std::string message); + void mediaProducerPause(JNIEnv* env, std::string producerId); + void mediaProducerResume(JNIEnv* env, std::string producerId); + void mediaProducerClose(JNIEnv* env, std::string producerId); + void mediaConsumerPause(JNIEnv* env, std::string consumerId); + void mediaConsumerResume(JNIEnv* env, std::string consumerId); + void mediaConsumerClose(JNIEnv* env, std::string consumerId); void close(); }; diff --git a/taoyao-client-android/taoyao/media/src/main/cpp/include/RouterCallback.hpp b/taoyao-client-android/taoyao/media/src/main/cpp/include/RouterCallback.hpp index 0a40f83..c6c1f75 100644 --- a/taoyao-client-android/taoyao/media/src/main/cpp/include/RouterCallback.hpp +++ b/taoyao-client-android/taoyao/media/src/main/cpp/include/RouterCallback.hpp @@ -9,21 +9,21 @@ namespace acgist { class RouterCallback { public: - JNIEnv* env; + JavaVM* javaVM; jobject routerCallback; public: - void enterCallback(std::string rtpCapabilities, std::string sctpCapabilities); - void sendTransportConnectCallback(std::string transportId, std::string dtlsParameters); - std::string sendTransportProduceCallback(std::string kind, std::string transportId, std::string rtpParameters); - void recvTransportConnectCallback(std::string transportId, std::string dtlsParameters); - void producerNewCallback(std::string kind, std::string producerId, webrtc::MediaStreamTrackInterface* producerMediaTrackPointer); - void producerPauseCallback(std::string producerId); - void producerResumeCallback(std::string producerId); - void producerCloseCallback(std::string producerId); - void consumerNewCallback(std::string message, webrtc::MediaStreamTrackInterface* consumerMediaTrackPointer); - void consumerPauseCallback(std::string consumerId); - void consumerResumeCallback(std::string consumerId); - void consumerCloseCallback(std::string consumerId); + void enterRoomCallback(JNIEnv* env, std::string rtpCapabilities, std::string sctpCapabilities); + void sendTransportConnectCallback(JNIEnv* env, std::string transportId, std::string dtlsParameters); + void recvTransportConnectCallback(JNIEnv* env, std::string transportId, std::string dtlsParameters); + std::string sendTransportProduceCallback(JNIEnv* env, std::string kind, std::string transportId, std::string rtpParameters); + void producerNewCallback(JNIEnv* env, std::string kind, std::string producerId, mediasoupclient::Producer* producerPointer, webrtc::MediaStreamTrackInterface* producerMediaTrackPointer); + void producerCloseCallback(JNIEnv* env, std::string producerId); + void producerPauseCallback(JNIEnv* env, std::string producerId); + void producerResumeCallback(JNIEnv* env, std::string producerId); + void consumerNewCallback(JNIEnv* env, std::string message, mediasoupclient::Consumer* consumerPointer, webrtc::MediaStreamTrackInterface* consumerMediaTrackPointer); + void consumerCloseCallback(JNIEnv* env, std::string consumerId); + void consumerPauseCallback(JNIEnv* env, std::string consumerId); + void consumerResumeCallback(JNIEnv* env, std::string consumerId); }; } \ No newline at end of file diff --git a/taoyao-client-android/taoyao/media/src/main/cpp/webrtc/Room.cpp b/taoyao-client-android/taoyao/media/src/main/cpp/webrtc/Room.cpp index ccb0ecf..a22ff62 100644 --- a/taoyao-client-android/taoyao/media/src/main/cpp/webrtc/Room.cpp +++ b/taoyao-client-android/taoyao/media/src/main/cpp/webrtc/Room.cpp @@ -19,25 +19,30 @@ namespace acgist { std::future OnConnect(mediasoupclient::Transport* transport, const nlohmann::json& dtlsParameters) override { const std::string cTransportId = transport->GetId(); const std::string cDtlsParameters = dtlsParameters.dump(); - this->room->sendTransportConnectCallback(cTransportId, cDtlsParameters); + JNIEnv* env; + this->room->javaVM->AttachCurrentThread(&env, nullptr); + this->room->sendTransportConnectCallback(env, cTransportId, cDtlsParameters); + this->room->javaVM->DetachCurrentThread(); return std::future(); } void OnConnectionStateChange(mediasoupclient::Transport* transport, const std::string& connectionState) override { - // 状态变化 + // TODO:restartIce? } std::future OnProduce(mediasoupclient::SendTransport* transport, const std::string& kind, nlohmann::json rtpParameters, const nlohmann::json& appData) override { const std::string cTransportId = transport->GetId(); const std::string cRtpParameters = rtpParameters.dump(); - std::string result = this->room->sendTransportProduceCallback(kind, cTransportId, cRtpParameters); + JNIEnv* env; + this->room->javaVM->AttachCurrentThread(&env, nullptr); + std::string result = this->room->sendTransportProduceCallback(env, kind, cTransportId, cRtpParameters); + this->room->javaVM->DetachCurrentThread(); std::promise promise; promise.set_value(result); return promise.get_future(); } std::future OnProduceData(mediasoupclient::SendTransport* transport, const nlohmann::json& sctpStreamParameters, const std::string& label, const std::string& protocol, const nlohmann::json& appData) override { - // 数据生产 return std::future(); } @@ -59,12 +64,14 @@ namespace acgist { std::future OnConnect(mediasoupclient::Transport* transport, const nlohmann::json& dtlsParameters) override { const std::string cTransportId = transport->GetId(); const std::string cDtlsParameters = dtlsParameters.dump(); - this->room->recvTransportConnectCallback(cTransportId, cDtlsParameters); + JNIEnv* env; + this->room->javaVM->AttachCurrentThread(&env, nullptr); + this->room->recvTransportConnectCallback(env, cTransportId, cDtlsParameters); + this->room->javaVM->DetachCurrentThread(); return std::future(); } void OnConnectionStateChange(mediasoupclient::Transport* transport, const std::string& connectionState) override { - // 状态变化 } }; @@ -83,7 +90,11 @@ namespace acgist { } void OnTransportClose(mediasoupclient::Producer* producer) override { - + producer->Close(); + JNIEnv* env; + this->room->javaVM->AttachCurrentThread(&env, nullptr); + this->room->producerCloseCallback(env, producer->GetId()); + this->room->javaVM->DetachCurrentThread(); } }; @@ -102,18 +113,22 @@ namespace acgist { } void OnTransportClose(mediasoupclient::Consumer* consumer) override { - + consumer->Close(); + JNIEnv* env; + this->room->javaVM->AttachCurrentThread(&env, nullptr); + this->room->consumerCloseCallback(env, consumer->GetId()); + this->room->javaVM->DetachCurrentThread(); } }; Room::Room( std::string roomId, - JNIEnv* env, + JavaVM* javaVM, jobject routerCallback ) { this->roomId = roomId; - this->env = env; + this->javaVM = javaVM; this->routerCallback = routerCallback; this->device = new mediasoupclient::Device(); this->sendListener = new SendListener(this); @@ -132,11 +147,15 @@ namespace acgist { delete this->videoProducer; delete this->producerListener; delete this->consumerListener; - this->env->DeleteLocalRef(this->routerCallback); - this->env->DeleteGlobalRef(this->routerCallback); + JNIEnv* env; + this->javaVM->AttachCurrentThread(&env, nullptr); + env->DeleteLocalRef(this->routerCallback); + env->DeleteGlobalRef(this->routerCallback); + this->javaVM->DetachCurrentThread(); } void Room::enter( + JNIEnv* env, std::string rtpCapabilities, webrtc::PeerConnectionFactoryInterface* factory, webrtc::PeerConnectionInterface::RTCConfiguration& rtcConfiguration @@ -150,10 +169,10 @@ namespace acgist { this->device->Load(json, &options); const std::string cRtpCapabilities = this->device->GetRtpCapabilities().dump(); const std::string cSctpCapabilities = this->device->GetSctpCapabilities().dump(); - this->enterCallback(cRtpCapabilities, cSctpCapabilities); + this->enterRoomCallback(env, cRtpCapabilities, cSctpCapabilities); } - void Room::createSendTransport(std::string body) { + void Room::createSendTransport(JNIEnv* env, std::string body) { nlohmann::json json = nlohmann::json::parse(body); this->sendTransport = this->device->CreateSendTransport( this->sendListener, @@ -166,7 +185,7 @@ namespace acgist { ); } - void Room::createRecvTransport(std::string body) { + void Room::createRecvTransport(JNIEnv* env, std::string body) { nlohmann::json json = nlohmann::json::parse(body); this->recvTransport = this->device->CreateRecvTransport( this->recvListener, @@ -179,7 +198,7 @@ namespace acgist { ); } - void Room::mediaProduceAudio(webrtc::MediaStreamInterface* mediaStream) { + void Room::mediaProduceAudio(JNIEnv* env, webrtc::MediaStreamInterface* mediaStream) { if(!this->device->CanProduce("audio")) { return; } @@ -195,9 +214,10 @@ namespace acgist { &codecOptions, nullptr ); + this->producerNewCallback(env, this->audioProducer->GetKind(), this->audioProducer->GetId(), this->audioProducer, this->audioProducer->GetTrack()); } - void Room::mediaProduceVideo(webrtc::MediaStreamInterface* mediaStream) { + void Room::mediaProduceVideo(JNIEnv* env, webrtc::MediaStreamInterface* mediaStream) { if(this->device->CanProduce("video")) { return; } @@ -230,9 +250,10 @@ namespace acgist { &codecOptions, nullptr ); + this->producerNewCallback(env, this->videoProducer->GetKind(), this->videoProducer->GetId(), this->videoProducer, this->videoProducer->GetTrack()); } - void Room::mediaConsume(std::string message) { + void Room::mediaConsume(JNIEnv* env, std::string message) { nlohmann::json json = nlohmann::json::parse(message); nlohmann::json body = json["body"]; mediasoupclient::Consumer* consumer = this->recvTransport->Consume( @@ -244,32 +265,95 @@ namespace acgist { ); this->consumers.insert({ consumer->GetId(), consumer }); webrtc::MediaStreamTrackInterface* trackPointer = consumer->GetTrack(); - this->consumerNewCallback(message, trackPointer); + this->consumerNewCallback(env, message, consumer, trackPointer); }; - void Room::mediaProducerPause(std::string producerId) { + void Room::mediaProducerPause(JNIEnv* env, std::string producerId) { + if(this->audioProducer->GetId() == producerId) { + this->audioProducer->Pause(); + } else if(this->videoProducer->GetId() == producerId) { + this->videoProducer->Pause(); + } else { + } + this->producerPauseCallback(env, producerId); } - void Room::mediaProducerResume(std::string producerId) { + void Room::mediaProducerResume(JNIEnv* env, std::string producerId) { + if(this->audioProducer->GetId() == producerId) { + this->audioProducer->Resume(); + } else if(this->videoProducer->GetId() == producerId) { + this->videoProducer->Resume(); + } else { + } + this->producerResumeCallback(env, producerId); } - void Room::mediaProducerClose(std::string producerId) { + void Room::mediaProducerClose(JNIEnv* env, std::string producerId) { + if(this->audioProducer->GetId() == producerId) { + this->audioProducer->Close(); + } else if(this->videoProducer->GetId() == producerId) { + this->videoProducer->Close(); + } else { + } + this->producerCloseCallback(env, producerId); } - void Room::mediaConsumerPause(std::string consumerId) { + void Room::mediaConsumerPause(JNIEnv* env, std::string consumerId) { + mediasoupclient::Consumer* consumer = this->consumers[consumerId]; + if(consumer == nullptr) { + return; + } + consumer->Pause(); + this->consumerPauseCallback(env, consumerId); } - void Room::mediaConsumerResume(std::string consumerId) { + void Room::mediaConsumerResume(JNIEnv* env, std::string consumerId) { + mediasoupclient::Consumer* consumer = this->consumers[consumerId]; + if(consumer == nullptr) { + return; + } + consumer->Resume(); + this->consumerResumeCallback(env, consumerId); } - void Room::mediaConsumerClose(std::string consumerId) { + void Room::mediaConsumerClose(JNIEnv* env, std::string consumerId) { + mediasoupclient::Consumer* consumer = this->consumers[consumerId]; + if(consumer == nullptr) { + return; + } + consumer->Close(); + this->consumerCloseCallback(env, consumerId); } void Room::close() { + this->audioProducer->Close(); + this->videoProducer->Close(); + std::map::iterator iterator; + for (iterator = this->consumers.begin(); iterator != this->consumers.end(); iterator++) { + iterator->second->Close(); + } + this->consumers.clear(); + this->sendTransport->Close(); + this->recvTransport->Close(); + } + + extern "C" JNIEXPORT jlong JNICALL + Java_com_acgist_taoyao_media_client_Room_nativeNewRoom( + JNIEnv* env, jobject me, + jstring jRoomId, jobject jRouterCallback + ) { + JavaVM* javaVM; + env->GetJavaVM(&javaVM); + jobject routerCallback = env->NewGlobalRef(jRouterCallback); + const char* roomId = env->GetStringUTFChars(jRoomId, nullptr); + Room* room = new Room(roomId, javaVM, routerCallback); + env->DeleteLocalRef(jRoomId); + env->ReleaseStringUTFChars(jRoomId, roomId); + return (jlong) room; } extern "C" JNIEXPORT void JNICALL - Java_com_acgist_taoyao_media_client_Room_nativeEnter( + Java_com_acgist_taoyao_media_client_Room_nativeEnterRoom( JNIEnv* env, jobject me, jlong nativeRoomPointer, jstring jRtpCapabilities, jlong factoryPointer, jobject jRtcConfiguration @@ -282,6 +366,7 @@ namespace acgist { // webrtc::jni::JavaToNativeRTCConfiguration(env, jRtcConfigurationRef, &rtcConfiguration); const char* rtpCapabilities = env->GetStringUTFChars(jRtpCapabilities, nullptr); room->enter( + env, rtpCapabilities, reinterpret_cast(factoryPointer), // (webrtc::PeerConnectionFactoryInterface*) factoryPointer, @@ -292,19 +377,6 @@ namespace acgist { env->DeleteLocalRef(jRtcConfiguration); } - extern "C" JNIEXPORT jlong JNICALL - Java_com_acgist_taoyao_media_client_Room_nativeNewRoom( - JNIEnv* env, jobject me, - jstring jRoomId, jobject jRouterCallback - ) { - jobject routerCallback = env->NewGlobalRef(jRouterCallback); - const char* roomId = env->GetStringUTFChars(jRoomId, nullptr); - Room* room = new Room(roomId, env, routerCallback); - env->DeleteLocalRef(jRoomId); - env->ReleaseStringUTFChars(jRoomId, roomId); - return (jlong) room; - } - extern "C" JNIEXPORT void JNICALL Java_com_acgist_taoyao_media_client_Room_nativeCloseRoom(JNIEnv* env, jobject me, jlong nativeRoomPointer) { Room* room = (Room*) nativeRoomPointer; @@ -316,7 +388,7 @@ namespace acgist { Java_com_acgist_taoyao_media_client_Room_nativeCreateSendTransport(JNIEnv* env, jobject me, jlong nativeRoomPointer, jstring jBody) { Room* room = (Room*) nativeRoomPointer; const char* body = env->GetStringUTFChars(jBody, nullptr); - room->createSendTransport(body); + room->createSendTransport(env, body); env->DeleteLocalRef(jBody); env->ReleaseStringUTFChars(jBody, body); } @@ -325,7 +397,7 @@ namespace acgist { Java_com_acgist_taoyao_media_client_Room_nativeCreateRecvTransport(JNIEnv* env, jobject me, jlong nativeRoomPointer, jstring jBody) { Room* room = (Room*) nativeRoomPointer; const char* body = env->GetStringUTFChars(jBody, nullptr); - room->createRecvTransport(body); + room->createRecvTransport(env, body); env->DeleteLocalRef(jBody); env->ReleaseStringUTFChars(jBody, body); } @@ -334,21 +406,21 @@ namespace acgist { Java_com_acgist_taoyao_media_client_Room_nativeMediaProduceAudio(JNIEnv* env, jobject me, jlong nativeRoomPointer, jlong mediaStreamPointer) { Room* room = (Room*) nativeRoomPointer; webrtc::MediaStreamInterface* mediaStream = reinterpret_cast(mediaStreamPointer); - room->mediaProduceAudio(mediaStream); + room->mediaProduceAudio(env, mediaStream); } extern "C" JNIEXPORT void JNICALL Java_com_acgist_taoyao_media_client_Room_nativeMediaProduceVideo(JNIEnv* env, jobject me, jlong nativeRoomPointer, jlong mediaStreamPointer) { Room* room = (Room*) nativeRoomPointer; webrtc::MediaStreamInterface* mediaStream = reinterpret_cast(mediaStreamPointer); - room->mediaProduceVideo(mediaStream); + room->mediaProduceVideo(env, mediaStream); } extern "C" JNIEXPORT void JNICALL Java_com_acgist_taoyao_media_client_Room_nativeMediaConsume(JNIEnv* env, jobject me, jlong nativeRoomPointer, jstring jMessage) { Room* room = (Room*) nativeRoomPointer; const char* message = env->GetStringUTFChars(jMessage, nullptr); - room->mediaConsume(message); + room->mediaConsume(env, message); env->DeleteLocalRef(jMessage); env->ReleaseStringUTFChars(jMessage, message); } @@ -357,7 +429,7 @@ namespace acgist { Java_com_acgist_taoyao_media_client_Room_nativeMediaProducerPause(JNIEnv* env, jobject me, jlong nativeRoomPointer, jstring jProducerId) { Room* room = (Room*) nativeRoomPointer; const char* producerId = env->GetStringUTFChars(jProducerId, nullptr); - room->mediaProducerPause(producerId); + room->mediaProducerPause(env, producerId); env->DeleteLocalRef(jProducerId); env->ReleaseStringUTFChars(jProducerId, producerId); } @@ -366,7 +438,7 @@ namespace acgist { Java_com_acgist_taoyao_media_client_Room_nativeMediaProducerResume(JNIEnv* env, jobject me, jlong nativeRoomPointer, jstring jProducerId) { Room* room = (Room*) nativeRoomPointer; const char* producerId = env->GetStringUTFChars(jProducerId, nullptr); - room->mediaProducerResume(producerId); + room->mediaProducerResume(env, producerId); env->DeleteLocalRef(jProducerId); env->ReleaseStringUTFChars(jProducerId, producerId); } @@ -375,7 +447,7 @@ namespace acgist { Java_com_acgist_taoyao_media_client_Room_nativeMediaProducerClose(JNIEnv* env, jobject me, jlong nativeRoomPointer, jstring jProducerId) { Room* room = (Room*) nativeRoomPointer; const char* producerId = env->GetStringUTFChars(jProducerId, nullptr); - room->mediaProducerClose(producerId); + room->mediaProducerClose(env, producerId); env->DeleteLocalRef(jProducerId); env->ReleaseStringUTFChars(jProducerId, producerId); } @@ -384,7 +456,7 @@ namespace acgist { Java_com_acgist_taoyao_media_client_Room_nativeMediaConsumerPause(JNIEnv* env, jobject me, jlong nativeRoomPointer, jstring jConsumerId) { Room* room = (Room*) nativeRoomPointer; const char* consumerId = env->GetStringUTFChars(jConsumerId, nullptr); - room->mediaConsumerPause(consumerId); + room->mediaConsumerPause(env, consumerId); env->DeleteLocalRef(jConsumerId); env->ReleaseStringUTFChars(jConsumerId, consumerId); } @@ -393,7 +465,7 @@ namespace acgist { Java_com_acgist_taoyao_media_client_Room_nativeMediaConsumerResume(JNIEnv* env, jobject me, jlong nativeRoomPointer, jstring jConsumerId) { Room* room = (Room*) nativeRoomPointer; const char* consumerId = env->GetStringUTFChars(jConsumerId, nullptr); - room->mediaConsumerResume(consumerId); + room->mediaConsumerResume(env, consumerId); env->DeleteLocalRef(jConsumerId); env->ReleaseStringUTFChars(jConsumerId, consumerId); } @@ -402,7 +474,7 @@ namespace acgist { Java_com_acgist_taoyao_media_client_Room_nativeMediaConsumerClose(JNIEnv* env, jobject me, jlong nativeRoomPointer, jstring jConsumerId) { Room* room = (Room*) nativeRoomPointer; const char* consumerId = env->GetStringUTFChars(jConsumerId, nullptr); - room->mediaConsumerClose(consumerId); + room->mediaConsumerClose(env, consumerId); env->DeleteLocalRef(jConsumerId); env->ReleaseStringUTFChars(jConsumerId, consumerId); } diff --git a/taoyao-client-android/taoyao/media/src/main/cpp/webrtc/RouterCallback.cpp b/taoyao-client-android/taoyao/media/src/main/cpp/webrtc/RouterCallback.cpp index 8596ac8..5c0bff4 100644 --- a/taoyao-client-android/taoyao/media/src/main/cpp/webrtc/RouterCallback.cpp +++ b/taoyao-client-android/taoyao/media/src/main/cpp/webrtc/RouterCallback.cpp @@ -2,99 +2,132 @@ namespace acgist { - void RouterCallback::enterCallback(std::string rtpCapabilities, std::string sctpCapabilities) { - jclass jCallbackClazz = this->env->GetObjectClass(this->routerCallback); - jmethodID recvTransportConnectCallback = this->env->GetMethodID(jCallbackClazz, "enterCallback", "(Ljava/lang/String;Ljava/lang/String;)V"); + void RouterCallback::enterRoomCallback(JNIEnv* env, std::string rtpCapabilities, std::string sctpCapabilities) { + jclass jCallbackClazz = env->GetObjectClass(this->routerCallback); + jmethodID recvTransportConnectCallback = env->GetMethodID(jCallbackClazz, "enterRoomCallback", "(Ljava/lang/String;Ljava/lang/String;)V"); const char* cRtpCapabilities = rtpCapabilities.data(); const char* cSctpCapabilities = sctpCapabilities.data(); - jstring jRtpCapabilities = this->env->NewStringUTF(cRtpCapabilities); - jstring jScrpCapabilities = this->env->NewStringUTF(cSctpCapabilities); - this->env->CallVoidMethod( + jstring jRtpCapabilities = env->NewStringUTF(cRtpCapabilities); + jstring jScrpCapabilities = env->NewStringUTF(cSctpCapabilities); + env->CallVoidMethod( this->routerCallback, recvTransportConnectCallback, jRtpCapabilities, jScrpCapabilities ); - this->env->DeleteLocalRef(jRtpCapabilities); - this->env->DeleteLocalRef(jScrpCapabilities); - this->env->DeleteLocalRef(jCallbackClazz); + env->DeleteLocalRef(jRtpCapabilities); + env->DeleteLocalRef(jScrpCapabilities); + env->DeleteLocalRef(jCallbackClazz); } - void RouterCallback::sendTransportConnectCallback(std::string transportId, std::string dtlsParameters) { - jclass jCallbackClazz = this->env->GetObjectClass(this->routerCallback); - jmethodID sendTransportConnectCallback = this->env->GetMethodID(jCallbackClazz, "sendTransportConnectCallback", "(Ljava/lang/String;Ljava/lang/String;)V"); + void RouterCallback::sendTransportConnectCallback(JNIEnv* env, std::string transportId, std::string dtlsParameters) { + jclass jCallbackClazz = env->GetObjectClass(this->routerCallback); + jmethodID sendTransportConnectCallback = env->GetMethodID(jCallbackClazz, "sendTransportConnectCallback", "(Ljava/lang/String;Ljava/lang/String;)V"); const char* cTransportId = transportId.data(); const char* cDtlsParameters = dtlsParameters.data(); - jstring jTransportId = this->env->NewStringUTF(cTransportId); - jstring jDtlsParameters = this->env->NewStringUTF(cDtlsParameters); - this->env->CallVoidMethod( + jstring jTransportId = env->NewStringUTF(cTransportId); + jstring jDtlsParameters = env->NewStringUTF(cDtlsParameters); + env->CallVoidMethod( this->routerCallback, sendTransportConnectCallback, jTransportId, jDtlsParameters ); - this->env->DeleteLocalRef(jTransportId); - this->env->DeleteLocalRef(jDtlsParameters); - this->env->DeleteLocalRef(jCallbackClazz); + env->DeleteLocalRef(jTransportId); + env->DeleteLocalRef(jDtlsParameters); + env->DeleteLocalRef(jCallbackClazz); } - std::string RouterCallback::sendTransportProduceCallback(std::string kind, std::string transportId, std::string rtpParameters) { - jclass jCallbackClazz = this->env->GetObjectClass(this->routerCallback); - jmethodID sendTransportProduceCallback = this->env->GetMethodID(jCallbackClazz, "sendTransportProduceCallback", "(Ljava/lang/String;Ljava/lang/String;)V"); + void RouterCallback::recvTransportConnectCallback(JNIEnv* env, std::string transportId, std::string dtlsParameters) { + jclass jCallbackClazz = env->GetObjectClass(this->routerCallback); + jmethodID recvTransportConnectCallback = env->GetMethodID(jCallbackClazz, "recvTransportConnectCallback", "(Ljava/lang/String;Ljava/lang/String;)V"); + const char* cTransportId = transportId.data(); + const char* cDtlsParameters = dtlsParameters.data(); + jstring jTransportId = env->NewStringUTF(cTransportId); + jstring jDtlsParameters = env->NewStringUTF(cDtlsParameters); + env->CallVoidMethod( + this->routerCallback, + recvTransportConnectCallback, + jTransportId, + jDtlsParameters + ); + env->DeleteLocalRef(jTransportId); + env->DeleteLocalRef(jDtlsParameters); + env->DeleteLocalRef(jCallbackClazz); + } + + std::string RouterCallback::sendTransportProduceCallback(JNIEnv* env, std::string kind, std::string transportId, std::string rtpParameters) { + jclass jCallbackClazz = env->GetObjectClass(this->routerCallback); + jmethodID sendTransportProduceCallback = env->GetMethodID(jCallbackClazz, "sendTransportProduceCallback", "(Ljava/lang/String;Ljava/lang/String;)V"); const char* cKind = kind.data(); const char* cTransportId = transportId.data(); const char* cRtpParameters = rtpParameters.data(); - jstring jKind = this->env->NewStringUTF(cKind); - jstring jTransportId = this->env->NewStringUTF(cTransportId); - jstring jRtpParameters = this->env->NewStringUTF(cRtpParameters); - jstring jResult = (jstring) this->env->CallObjectMethod( + jstring jKind = env->NewStringUTF(cKind); + jstring jTransportId = env->NewStringUTF(cTransportId); + jstring jRtpParameters = env->NewStringUTF(cRtpParameters); + jstring jResult = (jstring) env->CallObjectMethod( this->routerCallback, sendTransportProduceCallback, jKind, jTransportId, jRtpParameters ); - const char* result = this->env->GetStringUTFChars(jResult, nullptr); - this->env->DeleteLocalRef(jResult); - this->env->ReleaseStringUTFChars(jResult, result); - this->env->DeleteLocalRef(jKind); - this->env->DeleteLocalRef(jTransportId); - this->env->DeleteLocalRef(jRtpParameters); - this->env->DeleteLocalRef(jCallbackClazz); + const char* result = env->GetStringUTFChars(jResult, nullptr); + env->DeleteLocalRef(jResult); + env->ReleaseStringUTFChars(jResult, result); + env->DeleteLocalRef(jKind); + env->DeleteLocalRef(jTransportId); + env->DeleteLocalRef(jRtpParameters); + env->DeleteLocalRef(jCallbackClazz); return result; } - void RouterCallback::recvTransportConnectCallback(std::string transportId, std::string dtlsParameters) { - jclass jCallbackClazz = this->env->GetObjectClass(this->routerCallback); - jmethodID recvTransportConnectCallback = this->env->GetMethodID(jCallbackClazz, "recvTransportConnectCallback", "(Ljava/lang/String;Ljava/lang/String;)V"); - const char* cTransportId = transportId.data(); - const char* cDtlsParameters = dtlsParameters.data(); - jstring jTransportId = this->env->NewStringUTF(cTransportId); - jstring jDtlsParameters = this->env->NewStringUTF(cDtlsParameters); - this->env->CallVoidMethod( + void RouterCallback::producerNewCallback(JNIEnv* env, std::string kind, std::string producerId, mediasoupclient::Producer* producerPointer, webrtc::MediaStreamTrackInterface* producerMediaTrackPointer) { + jclass jCallbackClazz = env->GetObjectClass(this->routerCallback); + jmethodID producerNewCallback = env->GetMethodID(jCallbackClazz, "producerNewCallback", "(Ljava/lang/String;java/lang/String;J;J;)V"); + const char* cKind = kind.data(); + jstring jKind = env->NewStringUTF(cKind); + const char* cProducerId = producerId.data(); + jstring jProducerId = env->NewStringUTF(cProducerId); + env->CallVoidMethod( this->routerCallback, - recvTransportConnectCallback, - jTransportId, - jDtlsParameters + producerNewCallback, + jKind, + jProducerId, + (jlong) producerPointer, + (jlong) producerMediaTrackPointer ); - this->env->DeleteLocalRef(jTransportId); - this->env->DeleteLocalRef(jDtlsParameters); - this->env->DeleteLocalRef(jCallbackClazz); + env->DeleteLocalRef(jKind); + env->DeleteLocalRef(jProducerId); + env->DeleteLocalRef(jCallbackClazz); } - void RouterCallback::consumerNewCallback(std::string message, webrtc::MediaStreamTrackInterface* consumerMediaTrackPointer) { - jclass jCallbackClazz = this->env->GetObjectClass(this->routerCallback); - jmethodID consumerNewCallback = this->env->GetMethodID(jCallbackClazz, "consumerNewCallback", "(Ljava/lang/String;J;)V"); + void RouterCallback::producerCloseCallback(JNIEnv* env, std::string producerId) {} + + void RouterCallback::producerPauseCallback(JNIEnv* env, std::string producerId) {} + + void RouterCallback::producerResumeCallback(JNIEnv* env, std::string producerId) {} + + void RouterCallback::consumerNewCallback(JNIEnv* env, std::string message, mediasoupclient::Consumer* consumerPointer, webrtc::MediaStreamTrackInterface* consumerMediaTrackPointer) { + jclass jCallbackClazz = env->GetObjectClass(this->routerCallback); + jmethodID consumerNewCallback = env->GetMethodID(jCallbackClazz, "consumerNewCallback", "(Ljava/lang/String;J;J;)V"); const char* cMessage = message.data(); - jstring jMessage = this->env->NewStringUTF(cMessage); - this->env->CallVoidMethod( + jstring jMessage = env->NewStringUTF(cMessage); + env->CallVoidMethod( this->routerCallback, consumerNewCallback, jMessage, + (jlong) consumerPointer, (jlong) consumerMediaTrackPointer ); - this->env->DeleteLocalRef(jMessage); - this->env->DeleteLocalRef(jCallbackClazz); + env->DeleteLocalRef(jMessage); + env->DeleteLocalRef(jCallbackClazz); } + void RouterCallback::consumerCloseCallback(JNIEnv* env, std::string producerId) {} + + void RouterCallback::consumerPauseCallback(JNIEnv* env, std::string producerId) {} + + void RouterCallback::consumerResumeCallback(JNIEnv* env, std::string producerId) {} + } \ 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 eb30fec..e0d3b57 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,16 +2,22 @@ package com.acgist.taoyao.media; import android.content.Context; import android.content.Intent; -import android.media.AudioFormat; import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.media.projection.MediaProjection; import android.os.Handler; import android.os.Message; +import android.provider.MediaStore; import android.util.Log; +import com.acgist.taoyao.boot.utils.DateUtils; +import com.acgist.taoyao.media.client.PhotographClient; import com.acgist.taoyao.media.client.RecordClient; 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.config.WebrtcProperties; import com.acgist.taoyao.media.signal.ITaoyao; import org.webrtc.AudioSource; @@ -24,7 +30,6 @@ import org.webrtc.DefaultVideoEncoderFactory; import org.webrtc.EglBase; import org.webrtc.MediaConstraints; import org.webrtc.MediaStream; -import org.webrtc.MediaStreamTrack; import org.webrtc.PeerConnectionFactory; import org.webrtc.RendererCommon; import org.webrtc.ScreenCapturerAndroid; @@ -33,69 +38,24 @@ import org.webrtc.SurfaceViewRenderer; import org.webrtc.VideoCapturer; import org.webrtc.VideoDecoderFactory; import org.webrtc.VideoEncoderFactory; +import org.webrtc.VideoFrame; import org.webrtc.VideoSource; import org.webrtc.VideoTrack; import org.webrtc.audio.JavaAudioDeviceModule; +import java.time.LocalDateTime; import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.IntStream; +import java.util.function.Consumer; /** * 媒体来源管理器 * * @author acgist - *

- * https://zhuanlan.zhihu.com/p/82446482 - * https://www.jianshu.com/p/97acd9a51909 - * https://juejin.cn/post/7036308428305727519 - * 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/csdn_shen0221/article/details/120331004 - * https://blog.csdn.net/csdn_shen0221/article/details/119982257 - *

+ * * TODO:动态码率(BITRATE_MODE_VBR、BITRATE_MODE) */ public final class MediaManager { - /** - * 视频来源类型 - * - * @author acgist - */ - public enum Type { - - /** - * 文件共享:FileVideoCapturer - */ - FILE, - /** - * 后置摄像头:CameraVideoCapturer - */ - BACK, - /** - * 前置摄像头:CameraVideoCapturer - */ - FRONT, - /** - * 屏幕共享:ScreenCapturerAndroid - */ - SCREEN; - - /** - * @return 是否是摄像头 - */ - public boolean isCamera() { - return this == BACK || this == FRONT; - } - - } - private static final MediaManager INSTANCE = new MediaManager(); public static final MediaManager getInstance() { @@ -106,6 +66,10 @@ public final class MediaManager { * 当前终端数量 */ private volatile int clientCount; + /** + * 当前媒体共享数量 + */ + private volatile int shareClientCount; /** * 是否打开音频播放 */ @@ -130,10 +94,18 @@ public final class MediaManager { * 是否生产视频 */ private boolean videoProduce; + /** + * 视频路径 + */ + private String imagePath; + /** + * 图片路径 + */ + private String videoPath; /** * 视频来源类型 */ - private Type type; + private VideoSourceType videoSourceType; /** * 传输通道类型 */ @@ -150,24 +122,73 @@ public final class MediaManager { * 上下文 */ private Context context; + /** + * 媒体配置 + */ + private MediaProperties mediaProperties; + /** + * 音频配置 + */ + private MediaAudioProperties mediaAudioProperties; + /** + * 视频配置 + */ + private MediaVideoProperties mediaVideoProperties; + /** + * WebRTC配置 + */ + private WebrtcProperties webrtcProperties; /** * EGL */ private EglBase eglBase; - /** - * 录制终端 - */ - private RecordClient recordClient; /** * 媒体流:声音、视频 */ private MediaStream mediaStream; + /** + * 音频Track + */ + private AudioTrack audioTrack; + /** + * 音频来源 + */ private AudioSource audioSource; + /** + * 视频Track + */ + private VideoTrack videoTrack; + /** + * 视频来源 + */ private VideoSource videoSource; /** * 视频捕获 */ private VideoCapturer videoCapturer; + /** + * 录制终端 + */ + private RecordClient recordClient; + /** + * 录制视频Track + */ + private VideoTrack recordVideoTrack; + /** + * 录制视频来源 + */ + private VideoSource recordVideoSource; + /** + * 录制视频捕获 + */ + private VideoCapturer recordVideoCapturer; + /** + * 拍照终端 + */ + private PhotographClient photographClient; + /** + * 视频来源 + */ private SurfaceTextureHelper surfaceTextureHelper; /** * PeerConnectionFactory @@ -204,6 +225,14 @@ public final class MediaManager { private MediaManager() { this.clientCount = 0; + this.shareClientCount = 0; + } + + /** + * @return 是否可用 + */ + public boolean available() { + return this.handler != null && this.context != null && this.taoyao != null; } /** @@ -221,6 +250,7 @@ public final class MediaManager { boolean playAudio, boolean playVideo, boolean audioConsume, boolean videoConsume, boolean audioProduce, boolean videoProduce, + String imagePath, String videoPath, TransportType transportType ) { this.handler = handler; @@ -231,9 +261,12 @@ public final class MediaManager { this.videoConsume = videoConsume; this.audioProduce = audioProduce; this.videoProduce = videoProduce; + this.imagePath = imagePath; + this.videoPath = videoPath; this.transportType = transportType; PeerConnectionFactory.initialize( PeerConnectionFactory.InitializationOptions.builder(this.context) +// .setFieldTrials("WebRTC-H264HighProfile/Enabled/") // .setEnableInternalTracer(true) .createInitializationOptions() ); @@ -246,25 +279,24 @@ public final class MediaManager { this.taoyao = taoyao; } - /** - * @return 是否可用 - */ - public boolean available() { - return this.handler != null && this.context != null && this.taoyao != null; - } - /** * 新建终端 - * 第一个终端进入时没有初始化时,初始化所有资源。 * - * @param type 视频类型 + * @param videoSourceType 视频类型 * * @return PeerConnectionFactory PeerConnectionFactory */ - public PeerConnectionFactory newClient(Type type) { + public PeerConnectionFactory newClient(VideoSourceType videoSourceType) { synchronized (this) { + while(this.mediaProperties == null) { + try { + this.wait(1000); + } catch (InterruptedException e) { + Log.e(MediaManager.class.getSimpleName(), "等待配置异常", e); + } + } if (this.clientCount <= 0) { - this.initMedia(type); + this.initMedia(videoSourceType); this.nativeInit(); } this.clientCount++; @@ -294,36 +326,14 @@ public final class MediaManager { return this.recordClient != null; } - public RecordClient startRecord(String path, String filename) { - synchronized (this) { - this.recordClient = new RecordClient(path, filename, this.taoyao, this.handler); - this.recordClient.start(); - return this.recordClient; - } - } - - public void stopRecord() { - synchronized (this) { - this.recordClient.close(); - this.recordClient = null; - } - } - - /** - * @return 照片路径 - */ - public String photograph() { - return null; - } - /** * 加载媒体 * - * @param type 视频来源类型 + * @param videoSourceType 视频来源类型 */ - private void initMedia(Type type) { - Log.i(MediaManager.class.getSimpleName(), "加载媒体:" + type); - this.type = type; + private void initMedia(VideoSourceType videoSourceType) { + Log.i(MediaManager.class.getSimpleName(), "加载媒体:" + videoSourceType); + this.videoSourceType = videoSourceType; this.eglBase = EglBase.create(); final VideoDecoderFactory videoDecoderFactory = new DefaultVideoDecoderFactory(this.eglBase.getEglBaseContext()); final VideoEncoderFactory videoEncoderFactory = new DefaultVideoEncoderFactory(this.eglBase.getEglBaseContext(), true, true); @@ -360,39 +370,12 @@ public final class MediaManager { this.initVideo(); } - /** - * 切换视频来源 - * - * @param type 来源类型 - */ - public void exchange(Type type) { - if (this.type == type) { - return; - } - this.type = type; - Log.i(MediaManager.class.getSimpleName(), "设置视频来源:" + type); - if (this.type.isCamera() && type.isCamera()) { - // TODO:测试是否需要完全重置 - final CameraVideoCapturer cameraVideoCapturer = (CameraVideoCapturer) this.videoCapturer; - cameraVideoCapturer.switchCamera(new CameraVideoCapturer.CameraSwitchHandler() { - @Override - public void onCameraSwitchDone(boolean success) { - } - @Override - public void onCameraSwitchError(String message) { - } - }); - } else { - this.initVideo(); - } - } - /** * 加载音频 */ private void initAudio() { // 关闭音频 - this.closeAudioTrack(); + this.closeAudio(); // 加载音频 final MediaConstraints mediaConstraints = new MediaConstraints(); // 高音过滤 @@ -410,43 +393,46 @@ public final class MediaManager { // mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression2", "true")); // mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googTypingNoiseDetection", "true")); this.audioSource = this.peerConnectionFactory.createAudioSource(mediaConstraints); - final AudioTrack audioTrack = this.peerConnectionFactory.createAudioTrack("TaoyaoA0", this.audioSource); -// audioTrack.setVolume(100); - audioTrack.setEnabled(true); - this.mediaStream.addTrack(audioTrack); - Log.i(MediaManager.class.getSimpleName(), "加载音频:" + audioTrack.id()); + this.audioTrack = this.peerConnectionFactory.createAudioTrack("TaoyaoA0", this.audioSource); + this.audioTrack.setVolume(Config.DEFAULT_VOLUME); + this.audioTrack.setEnabled(true); + this.mediaStream.addTrack(this.audioTrack); + Log.i(MediaManager.class.getSimpleName(), "加载音频:" + this.audioTrack.id()); } /** * 加载视频 */ private void initVideo() { - this.closeVideoTrack(); - if (this.videoCapturer != null) { - this.videoCapturer.dispose(); - } - Log.i(MediaManager.class.getSimpleName(), "加载视频:" + this.type); - if (this.type.isCamera()) { + // 关闭视频 + this.closeVideo(); + // 加载视频 + Log.i(MediaManager.class.getSimpleName(), "加载视频:" + this.videoSourceType); + if (this.videoSourceType.isCamera()) { this.initCamera(); - } else if (this.type == Type.FILE) { - // 文件 - } else if (this.type == Type.SCREEN) { + } else if (this.videoSourceType == VideoSourceType.FILE) { + } else if (this.videoSourceType == VideoSourceType.SCREEN) { final Message message = new Message(); message.what = Config.WHAT_SCREEN_CAPTURE; this.handler.sendMessage(message); } else { - // 其他类型 + // 其他来源 } } + /** + * 加载摄像头 + */ private void initCamera() { final CameraEnumerator cameraEnumerator = new Camera2Enumerator(this.context); final String[] names = cameraEnumerator.getDeviceNames(); for (String name : names) { - if (this.type == Type.FRONT && cameraEnumerator.isFrontFacing(name)) { + if (this.videoSourceType == VideoSourceType.FRONT && cameraEnumerator.isFrontFacing(name)) { this.videoCapturer = cameraEnumerator.createCapturer(name, new MediaCameraEventsHandler()); - } else if (this.type == Type.BACK && cameraEnumerator.isBackFacing(name)) { + this.recordVideoCapturer = cameraEnumerator.createCapturer(name, new MediaCameraEventsHandler()); + } else if (this.videoSourceType == VideoSourceType.BACK && cameraEnumerator.isBackFacing(name)) { this.videoCapturer = cameraEnumerator.createCapturer(name, new MediaCameraEventsHandler()); + this.recordVideoCapturer = cameraEnumerator.createCapturer(name, new MediaCameraEventsHandler()); } else { // 忽略其他摄像头 } @@ -454,14 +440,14 @@ public final class MediaManager { this.initVideoTrack(); } - public void screenRecord(Intent intent) { - this.videoCapturer = new ScreenCapturerAndroid(intent, new MediaProjection.Callback() { - @Override - public void onStop() { - super.onStop(); - Log.i(MediaManager.class.getSimpleName(), "停止屏幕捕获"); - } - }); + /** + * 加载屏幕 + * + * @param intent Intent + */ + public void initScreen(Intent intent) { + this.videoCapturer = new ScreenCapturerAndroid(intent, new ScreenCallback()); + this.recordVideoCapturer = new ScreenCapturerAndroid(intent, new ScreenCallback()); this.initVideoTrack(); } @@ -473,26 +459,193 @@ public final class MediaManager { this.surfaceTextureHelper = SurfaceTextureHelper.create("MediaVideoThread", this.eglBase.getEglBaseContext()); // this.surfaceTextureHelper.setTextureSize(); // this.surfaceTextureHelper.setFrameRotation(); + // 次码流 this.videoSource = this.peerConnectionFactory.createVideoSource(this.videoCapturer.isScreencast()); - // 美颜水印 -// this.videoSource.setVideoProcessor(); this.videoCapturer.initialize(this.surfaceTextureHelper, this.context, this.videoSource.getCapturerObserver()); - this.videoCapturer.startCapture(480, 640, 30); - final VideoTrack videoTrack = this.peerConnectionFactory.createVideoTrack("TaoyaoV0", this.videoSource); - videoTrack.addSink(videoFrame -> { + this.videoTrack = this.peerConnectionFactory.createVideoTrack("TaoyaoV0", this.videoSource); + this.videoTrack.setEnabled(true); + this.mediaStream.addTrack(this.videoTrack); + Log.i(MediaManager.class.getSimpleName(), "加载视频(次码流):" + this.videoTrack.id()); + // 主码流 + this.recordVideoSource = this.peerConnectionFactory.createVideoSource(this.recordVideoCapturer.isScreencast()); + this.recordVideoCapturer.initialize(this.surfaceTextureHelper, this.context, this.recordVideoSource.getCapturerObserver()); + this.recordVideoTrack = this.peerConnectionFactory.createVideoTrack("TaoyaoV1", this.recordVideoSource); + this.recordVideoTrack.addSink(videoFrame -> { if(this.recordClient != null) { this.recordClient.putVideo(videoFrame); } + if(this.photographClient != null) { + synchronized (this.photographClient) { + this.photographClient.photograph(videoFrame); + this.photographClient = null; + } + } }); - videoTrack.setEnabled(true); - this.mediaStream.addTrack(videoTrack); - Log.i(MediaManager.class.getSimpleName(), "加载视频:" + videoTrack.id()); + this.recordVideoTrack.setEnabled(true); + Log.i(MediaManager.class.getSimpleName(), "加载视频(主码流):" + this.recordVideoTrack.id()); + // 视频处理 +// this.videoSource.setVideoProcessor(); + } + + /** + * 更新配置 + * + * @param mediaProperties 媒体配置 + * @param mediaAudioProperties 音频配置 + * @param mediaVideoProperties 视频配置 + */ + public void updateMediaConfig(MediaProperties mediaProperties, MediaAudioProperties mediaAudioProperties, MediaVideoProperties mediaVideoProperties) { + this.mediaProperties = mediaProperties; + this.updateAudioConfig(mediaAudioProperties); + this.updateVideoConfig(mediaVideoProperties); + synchronized (this) { + this.notifyAll(); + } + } + + public void updateAudioConfig(MediaAudioProperties mediaAudioProperties) { + this.mediaAudioProperties = mediaAudioProperties; + } + + public void updateVideoConfig(MediaVideoProperties mediaVideoProperties) { + this.mediaVideoProperties = mediaVideoProperties; + if(this.videoCapturer != null) { + try { + this.videoCapturer.stopCapture(); + } catch (InterruptedException e) { + Log.e(MediaManager.class.getSimpleName(), "暂停视频捕获异常", e); + } + this.videoCapturer.startCapture(this.mediaVideoProperties.getHeight(), this.mediaVideoProperties.getWidth(), this.mediaVideoProperties.getFrameRate()); + } + } + + public void updateWebrtcConfig(WebrtcProperties webrtcProperties) { + this.webrtcProperties = webrtcProperties; + } + + /** + * 切换视频来源 + * + * @param videoSourceType 来源类型 + */ + public void updateVideoSource(VideoSourceType videoSourceType) { + if (this.videoSourceType == videoSourceType) { + return; + } + this.videoSourceType = videoSourceType; + Log.i(MediaManager.class.getSimpleName(), "设置视频来源:" + videoSourceType); + if (this.videoSourceType.isCamera() && videoSourceType.isCamera()) { + // TODO:测试是否需要完全重置 + final CameraVideoCapturer cameraVideoCapturer = (CameraVideoCapturer) this.videoCapturer; + cameraVideoCapturer.switchCamera(new CameraVideoCapturer.CameraSwitchHandler() { + @Override + public void onCameraSwitchDone(boolean success) { + } + @Override + public void onCameraSwitchError(String message) { + } + }); + } else { + this.initVideo(); + } } public MediaStream getMediaStream() { return this.mediaStream; } + public void startVideoCapture() { + synchronized (this) { + if(this.videoCapturer == null) { + return; + } + if(this.shareClientCount > 0) { + this.shareClientCount++; + return; + } else { + this.shareClientCount++; + this.videoCapturer.startCapture(this.mediaVideoProperties.getHeight(), this.mediaVideoProperties.getWidth(), this.mediaVideoProperties.getFrameRate()); + } + } + } + + public void stopVideoCapture() { + synchronized (this) { + if(this.videoCapturer == null) { + return; + } + if(this.shareClientCount <= 0) { + return; + } + this.shareClientCount--; + if(this.shareClientCount <= 0) { + try { + this.videoCapturer.stopCapture(); + } catch (InterruptedException e) { + Log.e(MediaManager.class.getSimpleName(), "关闭视频捕获(次码流)异常", e); + } + } + } + } + + public String photograph() { + synchronized (this) { + if(this.recordVideoCapturer == null) { + // 如果没有拉流不能拍照 + return null; + } + String filepath; + if(this.recordClient == null) { + final MediaVideoProperties mediaVideoProperties = this.mediaProperties.getVideos().get("fd-video"); + this.recordVideoCapturer.startCapture(mediaVideoProperties.getHeight(), mediaVideoProperties.getWidth(), mediaVideoProperties.getFrameRate()); + this.photographClient = new PhotographClient(this.imagePath); + filepath = this.photographClient.waitForPhotograph(); + try { + this.recordVideoCapturer.stopCapture(); + } catch (InterruptedException e) { + Log.e(MediaManager.class.getSimpleName(), "关闭视频捕获(主码流)异常", e); + } + } else { + this.photographClient = new PhotographClient(this.imagePath); + filepath = this.photographClient.waitForPhotograph(); + } + return filepath; + } + } + + public RecordClient startRecordVideoCapture() { + synchronized (this) { + if(this.recordClient != null) { + return this.recordClient; + } + this.recordClient = new RecordClient( + this.videoPath, + this.taoyao, + this.handler + ); + this.recordClient.start(); + final MediaVideoProperties mediaVideoProperties = this.mediaProperties.getVideos().get("fd-video"); + this.recordVideoCapturer.startCapture(mediaVideoProperties.getHeight(), mediaVideoProperties.getWidth(), mediaVideoProperties.getFrameRate()); + return this.recordClient; + } + } + + public void stopRecordVideoCapture() { + synchronized (this) { + 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); + } + } + } + } + /** * @param flag Config.WHAT_* * @param videoTrack 视频媒体流Track @@ -561,80 +714,75 @@ public final class MediaManager { /** * 关闭声音 */ - private void closeAudioTrack() { - synchronized (this.mediaStream.audioTracks) { - AudioTrack track; - final Iterator iterator = this.mediaStream.audioTracks.iterator(); - while (iterator.hasNext()) { - track = iterator.next(); - track.dispose(); - iterator.remove(); - } - } - } - - /** - * 关闭视频 - */ - private void closeVideoTrack() { - this.closeVideoTrack(this.mediaStream.videoTracks); - this.closeVideoTrack(this.mediaStream.preservedVideoTracks); - } - - /** - * 关闭视频 - * - * @param list 视频列表 - */ - private void closeVideoTrack(List list) { - synchronized (list) { - VideoTrack track; - final Iterator iterator = list.iterator(); - while (iterator.hasNext()) { - track = iterator.next(); - track.dispose(); - iterator.remove(); - } - } - } - - /** - * 释放资源 - */ - private void close() { - this.closeAudioTrack(); - this.closeVideoTrack(); - if (this.eglBase != null) { - this.eglBase.release(); - this.eglBase = null; + private void closeAudio() { + if(this.audioTrack != null) { + this.audioTrack.dispose(); + this.audioTrack = null; } if(this.audioSource != null) { this.audioSource.dispose(); this.audioSource = null; } + } + + /** + * 关闭视频 + */ + private void closeVideo() { + this.stopVideoCapture(); + this.stopRecordVideoCapture(); + if(this.videoTrack != null) { + this.videoTrack.dispose(); + this.videoTrack = null; + } if(this.videoSource != null) { this.videoSource.dispose(); this.videoSource = null; } - if (this.mediaStream != null) { - this.mediaStream.dispose(); - this.mediaStream = null; - } if (this.videoCapturer != null) { this.videoCapturer.dispose(); this.videoCapturer = null; } + if(this.recordVideoTrack != null) { + this.recordVideoTrack.dispose(); + this.recordVideoTrack = null; + } + if(this.recordVideoSource != null) { + this.recordVideoSource.dispose(); + this.recordVideoSource = null; + } + if(this.recordVideoCapturer != null) { + this.recordVideoCapturer.dispose(); + this.recordVideoCapturer = null; + } if(this.surfaceTextureHelper != null) { this.surfaceTextureHelper.dispose(); this.surfaceTextureHelper = null; } + } + + private void closeMedia() { + if (this.eglBase != null) { + this.eglBase.release(); + this.eglBase = null; + } + if (this.mediaStream != null) { + this.mediaStream.dispose(); + this.mediaStream = null; + } if (this.peerConnectionFactory != null) { this.peerConnectionFactory.dispose(); this.peerConnectionFactory = null; } } - public void shutdown() { + /** + * 释放资源 + */ + private void close() { + this.closeAudio(); + this.closeVideo(); + this.closeMedia(); // PeerConnectionFactory.shutdownInternalTracer(); // PeerConnectionFactory.stopInternalTracingCapture(); } @@ -660,6 +808,7 @@ public final class MediaManager { @Override public void onCameraOpening(String message) { + Log.i(MediaManager.class.getSimpleName(), "开始视频捕获"); } @Override @@ -668,6 +817,20 @@ public final class MediaManager { @Override public void onCameraClosed() { + Log.i(MediaManager.class.getSimpleName(), "停止视频捕获"); + } + + } + + /** + * 屏幕录制回调 + */ + private static class ScreenCallback extends MediaProjection.Callback { + + @Override + public void onStop() { + super.onStop(); + Log.i(MediaManager.class.getSimpleName(), "停止屏幕捕获"); } } diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/RouterCallback.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/RouterCallback.java index 9db169a..03c3d47 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/RouterCallback.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/RouterCallback.java @@ -1,7 +1,5 @@ package com.acgist.taoyao.media; -import org.webrtc.MediaStream; - /** * 路由回调 * @@ -9,17 +7,17 @@ import org.webrtc.MediaStream; */ public interface RouterCallback { - default void enterCallback(String rtpCapabilities, String sctpCapabilities) {}; + default void enterRoomCallback(String rtpCapabilities, String sctpCapabilities) {}; default void sendTransportConnectCallback(String transportId, String dtlsParameters) {}; - default String sendTransportProduceCallback(String kind, String transportId, String rtpParameters) { return null; }; default void recvTransportConnectCallback(String transportId, String dtlsParameters) {}; - default void producerNewCallback(String kind, String producerId, long producerMediaTrackPointer) {}; + default String sendTransportProduceCallback(String kind, String transportId, String rtpParameters) { return null; }; + default void producerNewCallback(String kind, String producerId, long producerPointer, long producerMediaTrackPointer) {}; + default void producerCloseCallback(String producerId) {}; default void producerPauseCallback(String producerId) {}; default void producerResumeCallback(String producerId) {}; - default void producerCloseCallback(String producerId) {}; - default void consumerNewCallback(String message, long consumerMediaTrackPointer) {}; + default void consumerNewCallback(String message, long consumerPointer, long consumerMediaTrackPointer) {}; + default void consumerCloseCallback(String consumerId) {}; default void consumerPauseCallback(String consumerId) {}; default void consumerResumeCallback(String consumerId) {}; - default void consumerCloseCallback(String consumerId) {}; } diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/VideoSourceType.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/VideoSourceType.java new file mode 100644 index 0000000..188eecb --- /dev/null +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/VideoSourceType.java @@ -0,0 +1,34 @@ +package com.acgist.taoyao.media; + +/** + * 视频来源类型 + * + * @author acgist + */ +public enum VideoSourceType { + + /** + * 文件共享:FileVideoCapturer + */ + FILE, + /** + * 后置摄像头:CameraVideoCapturer + */ + BACK, + /** + * 前置摄像头:CameraVideoCapturer + */ + FRONT, + /** + * 屏幕共享:ScreenCapturerAndroid + */ + SCREEN; + + /** + * @return 是否是摄像头 + */ + public boolean isCamera() { + return this == BACK || this == FRONT; + } + +} diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/LocalClient.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/LocalClient.java index f5e7499..c659cf0 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/LocalClient.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/LocalClient.java @@ -2,12 +2,17 @@ package com.acgist.taoyao.media.client; import android.os.Handler; +import com.acgist.taoyao.boot.utils.ListUtils; import com.acgist.taoyao.media.config.Config; import com.acgist.taoyao.media.signal.ITaoyao; +import org.webrtc.AudioTrack; import org.webrtc.MediaStream; import org.webrtc.VideoTrack; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + /** * 房间本地终端 * @@ -19,9 +24,17 @@ public class LocalClient extends RoomClient { * 媒体流 */ protected MediaStream mediaStream; + /** + * 媒体流Track + * 生产者ID = 媒体流Track指针 + */ + protected final Map tracks; + protected long audioProducerPointer; + protected long videoProducerPointer; public LocalClient(String name, String clientId, ITaoyao taoyao, Handler handler) { super(name, clientId, taoyao, handler); + this.tracks = new ConcurrentHashMap<>(); } public MediaStream getMediaStream() { @@ -32,32 +45,38 @@ public class LocalClient extends RoomClient { this.mediaStream = mediaStream; } - @Override - public void playVideo() { - super.playVideo(); - if(this.mediaStream == null) { - return; - } - final VideoTrack videoTrack = this.mediaStream.videoTracks.get(0); - if(this.surfaceViewRenderer == null) { - this.surfaceViewRenderer = this.mediaManager.buildSurfaceViewRenderer(Config.WHAT_NEW_LOCAL_VIDEO, videoTrack); - } else { - videoTrack.setEnabled(true); - } - } - @Override public void playAudio() { super.playAudio(); if(this.mediaStream == null) { return; } - this.mediaStream.audioTracks.forEach(v -> v.setEnabled(true)); + ListUtils.getOnlyOne(this.mediaStream.audioTracks, audioTrack -> { + audioTrack.setEnabled(true); + return audioTrack; + }); + } + + @Override + public void playVideo() { + super.playVideo(); + if(this.mediaStream == null) { + return; + } + ListUtils.getOnlyOne(this.mediaStream.videoTracks, videoTrack -> { + if(this.surfaceViewRenderer == null) { + this.surfaceViewRenderer = this.mediaManager.buildSurfaceViewRenderer(Config.WHAT_NEW_LOCAL_VIDEO, videoTrack); + } else { + videoTrack.setEnabled(true); + } + return videoTrack; + }); } @Override public void close() { super.close(); + this.tracks.clear(); } } 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 new file mode 100644 index 0000000..5df4b51 --- /dev/null +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/PhotographClient.java @@ -0,0 +1,56 @@ +package com.acgist.taoyao.media.client; + +import android.os.Environment; +import android.provider.MediaStore; +import android.util.Log; + +import com.acgist.taoyao.boot.utils.DateUtils; + +import org.webrtc.VideoFrame; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; + +/** + * 拍照终端 + * + * @author acgist + */ +public class PhotographClient { + + private final String filename; + private final String filepath; + private VideoFrame videoFrame; + + public PhotographClient(String path) { + this.filename = DateUtils.format(LocalDateTime.now(), DateUtils.DateTimeStyle.YYYYMMDDHH24MMSS) + ".mp4"; + final Path filePath = Paths.get(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath(), path, filename); + final File parentFile = filePath.getParent().toFile(); + if(!parentFile.exists()) { + parentFile.mkdirs(); + } + this.filepath = filePath.toString(); + Log.i(RecordClient.class.getSimpleName(), "拍摄照片文件:" + this.filepath); + } + + public String photograph(VideoFrame videoFrame) { + synchronized (this) { + this.notifyAll(); + } + return this.filepath; + } + + public String waitForPhotograph() { + synchronized (this) { + try { + this.wait(5000); + } catch (InterruptedException e) { + Log.e(PhotographClient.class.getSimpleName(), "拍照等待异常", e); + } + } + return this.filepath; + } + +} 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 b1a6155..64cc373 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 @@ -10,7 +10,9 @@ import android.os.Handler; import android.os.HandlerThread; import android.util.Log; +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.TextureBufferImpl; @@ -23,13 +25,21 @@ 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.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 录像机 - *

+ * + * 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/120331004 + * https://blog.csdn.net/csdn_shen0221/article/details/119982257 * * @author acgist */ @@ -88,9 +98,9 @@ public class RecordClient extends Client { */ private final ExecutorService executorService; - public RecordClient(String path, String filename, ITaoyao taoyao, Handler handler) { + public RecordClient(String path, ITaoyao taoyao, Handler handler) { super("本地录像", "LocalRecordClient", taoyao, handler); - this.filename = filename; + this.filename = DateUtils.format(LocalDateTime.now(), DateUtils.DateTimeStyle.YYYYMMDDHH24MMSS) + ".mp4"; final Path filePath = Paths.get(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath(), path, filename); final File parentFile = filePath.getParent().toFile(); if(!parentFile.exists()) { @@ -107,7 +117,7 @@ public class RecordClient extends Client { return; } super.init(); - this.mediaManager.newClient(MediaManager.Type.BACK); + this.mediaManager.newClient(VideoSourceType.BACK); this.record(null, null, 1, 1); } } diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RemoteClient.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RemoteClient.java index 281b62e..3fb7df3 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RemoteClient.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RemoteClient.java @@ -1,16 +1,18 @@ package com.acgist.taoyao.media.client; import android.os.Handler; -import android.provider.MediaStore; +import com.acgist.taoyao.boot.utils.ListUtils; import com.acgist.taoyao.media.config.Config; import com.acgist.taoyao.media.signal.ITaoyao; import org.webrtc.MediaStreamTrack; import org.webrtc.VideoTrack; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; /** * 房间远程终端 @@ -24,6 +26,8 @@ public class RemoteClient extends RoomClient { * 消费者ID = 媒体流Track */ protected final Map tracks; + protected long audioConsumerPointer; + protected long videoConsumerPointer; public RemoteClient(String name, String clientId, ITaoyao taoyao, Handler handler) { super(name, clientId, taoyao, handler); @@ -31,28 +35,34 @@ public class RemoteClient extends RoomClient { } @Override - public void playVideo() { - super.playVideo(); - final VideoTrack videoTrack = (VideoTrack) this.tracks.values().stream() - .filter(v -> MediaStreamTrack.VIDEO_TRACK_KIND.equals(v.kind())) - .findFirst() - .orElse(null); - if(videoTrack == null) { - return; - } - if(this.surfaceViewRenderer == null) { - this.surfaceViewRenderer = this.mediaManager.buildSurfaceViewRenderer(Config.WHAT_NEW_REMOTE_VIDEO, videoTrack); - } else { - videoTrack.setEnabled(true); - } + public void playAudio() { + super.playAudio(); + ListUtils.getOnlyOne( + this.tracks.values().stream().filter(v -> MediaStreamTrack.AUDIO_TRACK_KIND.equals(v.kind())).collect(Collectors.toList()), + audioTrack -> { + audioTrack.setEnabled(true); + return audioTrack; + } + ); } @Override - public void playAudio() { - super.playAudio(); - this.tracks.values().stream() - .filter(v -> MediaStreamTrack.AUDIO_TRACK_KIND.equals(v.kind())) - .forEach(v -> v.setEnabled(true)); + public void playVideo() { + super.playVideo(); + ListUtils.getOnlyOne( + this.tracks.values().stream() + .filter(v -> MediaStreamTrack.VIDEO_TRACK_KIND.equals(v.kind())) + .map(v -> (VideoTrack) v) + .collect(Collectors.toList()), + videoTrack -> { + if(this.surfaceViewRenderer == null) { + this.surfaceViewRenderer = this.mediaManager.buildSurfaceViewRenderer(Config.WHAT_NEW_REMOTE_VIDEO, videoTrack); + } else { + videoTrack.setEnabled(true); + } + return videoTrack; + } + ); } @Override 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 0c5d953..2c463cb 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 @@ -9,6 +9,7 @@ import com.acgist.taoyao.boot.utils.MapUtils; import com.acgist.taoyao.boot.utils.PointerUtils; import com.acgist.taoyao.media.MediaManager; import com.acgist.taoyao.media.RouterCallback; +import com.acgist.taoyao.media.VideoSourceType; import com.acgist.taoyao.media.config.Config; import com.acgist.taoyao.media.signal.ITaoyao; @@ -19,7 +20,6 @@ import org.webrtc.PeerConnection; import org.webrtc.PeerConnectionFactory; import org.webrtc.VideoTrack; -import java.io.Closeable; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -81,7 +81,8 @@ public class Room extends CloseableClient implements RouterCallback { return true; } super.init(); - this.peerConnectionFactory = this.mediaManager.newClient(MediaManager.Type.BACK); + this.peerConnectionFactory = this.mediaManager.newClient(VideoSourceType.BACK); + this.mediaManager.startVideoCapture(); this.localClient = new LocalClient(this.name, this.clientId, this.taoyao, this.handler); this.localClient.setMediaStream(this.mediaManager.getMediaStream()); // STUN | TURN @@ -96,7 +97,7 @@ public class Room extends CloseableClient implements RouterCallback { return false; } final Object rtpCapabilities = MapUtils.get(response.body(), "rtpCapabilities"); - this.nativeEnter(this.nativeRoomPointer, JSONUtils.toJSON(rtpCapabilities), this.peerConnectionFactory.getNativePeerConnectionFactory(), this.rtcConfiguration); + this.nativeEnterRoom(this.nativeRoomPointer, JSONUtils.toJSON(rtpCapabilities), this.peerConnectionFactory.getNativePeerConnectionFactory(), this.rtcConfiguration); return true; } } @@ -194,6 +195,7 @@ public class Room extends CloseableClient implements RouterCallback { this.remoteClients.values().forEach(v -> this.closeRemoteClient(v.clientId)); this.remoteClients.clear(); this.localClient.close(); + this.mediaManager.stopVideoCapture(); this.mediaManager.closeClient(); } } @@ -207,7 +209,7 @@ public class Room extends CloseableClient implements RouterCallback { } public void mediaConsumerClose(Map body) { - + this.nativeMediaConsumerClose(this.nativeRoomPointer, MapUtils.get(body, "consumerId")); } public void mediaConsumerPause(String consumerId) { @@ -219,7 +221,7 @@ public class Room extends CloseableClient implements RouterCallback { } public void mediaConsumerPause(Map body) { - + this.nativeMediaConsumerPause(this.nativeRoomPointer, MapUtils.get(body, "consumerId")); } public void mediaConsumerResume(String consumerId) { @@ -231,7 +233,7 @@ public class Room extends CloseableClient implements RouterCallback { } public void mediaConsumerResume(Map body) { - + this.nativeMediaConsumerResume(this.nativeRoomPointer, MapUtils.get(body, "consumerId")); } public void mediaProducerClose(String producerId) { @@ -243,7 +245,7 @@ public class Room extends CloseableClient implements RouterCallback { } public void mediaProducerClose(Map body) { - + this.nativeMediaProducerClose(this.nativeRoomPointer, MapUtils.get(body, "producerId")); } public void mediaProducerPause(String producerId) { @@ -255,7 +257,7 @@ public class Room extends CloseableClient implements RouterCallback { } public void mediaProducerPause(Map body) { - + this.nativeMediaProducerPause(this.nativeRoomPointer, MapUtils.get(body, "producerId")); } public void mediaProducerResume(String producerId) { @@ -267,11 +269,11 @@ public class Room extends CloseableClient implements RouterCallback { } public void mediaProducerResume(Map body) { - + this.nativeMediaProducerResume(this.nativeRoomPointer, MapUtils.get(body, "producerId")); } @Override - public void enterCallback(String rtpCapabilities, String sctpCapabilities) { + public void enterRoomCallback(String rtpCapabilities, String sctpCapabilities) { this.rtpCapabilities = rtpCapabilities; this.sctpCapabilities = sctpCapabilities; this.taoyao.request(this.taoyao.buildMessage( @@ -293,13 +295,23 @@ public class Room extends CloseableClient implements RouterCallback { )); } + @Override + public void recvTransportConnectCallback(String transportId, String dtlsParameters) { + this.taoyao.request(this.taoyao.buildMessage( + "media::transport::webrtc::connect", + "roomId", this.roomId, + "transportId", transportId, + "dtlsParameters", JSONUtils.toMap(dtlsParameters) + )); + } + @Override public String sendTransportProduceCallback(String kind, String transportId, String rtpParameters) { final Message response = this.taoyao.request(this.taoyao.buildMessage( "media::produce", - "kind", kind, - "roomId", this.roomId, - "transportId", transportId, + "kind", kind, + "roomId", this.roomId, + "transportId", transportId, "rtpParameters", JSONUtils.toMap(rtpParameters) )); final Map body = response.body(); @@ -307,17 +319,18 @@ public class Room extends CloseableClient implements RouterCallback { } @Override - public void recvTransportConnectCallback(String transportId, String dtlsParameters) { - this.taoyao.request(this.taoyao.buildMessage( - "media::transport::webrtc::connect", - "roomId", this.roomId, - "transportId", transportId, - "dtlsParameters", JSONUtils.toMap(dtlsParameters) - )); + public void producerNewCallback(String kind, String producerId, long producerPointer, long producerMediaTrackPointer) { + this.localClient.tracks.put(producerId, producerMediaTrackPointer); + if(MediaStreamTrack.AUDIO_TRACK_KIND.equals(kind)) { + this.localClient.audioProducerPointer = producerPointer; + } else if(MediaStreamTrack.VIDEO_TRACK_KIND.equals(kind)) { + this.localClient.videoProducerPointer = producerPointer; + } else { + } } @Override - public void consumerNewCallback(String message, long consumerMediaTrackPointer) { + public void consumerNewCallback(String message, long consumerPointer, long consumerMediaTrackPointer) { final Message response = JSONUtils.toJava(message, Message.class); final Map body = response.body(); final String kind = MapUtils.get(body, "kind"); @@ -331,10 +344,14 @@ public class Room extends CloseableClient implements RouterCallback { if(MediaStreamTrack.AUDIO_TRACK_KIND.equals(kind)) { // WebRtcAudioTrack final AudioTrack audioTrack = new AudioTrack(consumerMediaTrackPointer); + audioTrack.setVolume(Config.DEFAULT_VOLUME); remoteClient.tracks.put(consumerId, audioTrack); + remoteClient.audioConsumerPointer = consumerPointer; + remoteClient.playAudio(); } else if(MediaStreamTrack.VIDEO_TRACK_KIND.equals(kind)) { final VideoTrack videoTrack = new VideoTrack(consumerMediaTrackPointer); remoteClient.tracks.put(consumerId, videoTrack); + remoteClient.videoConsumerPointer = consumerPointer; remoteClient.playVideo(); } else { Log.w(Room.class.getSimpleName(), "未知媒体类型:" + kind); @@ -344,8 +361,8 @@ public class Room extends CloseableClient implements RouterCallback { this.taoyao.push(response); } - private native void nativeEnter(long nativePointer, String rtpCapabilities, long peerConnectionFactoryPointer, PeerConnection.RTCConfiguration rtcConfiguration); private native long nativeNewRoom(String roomId, RouterCallback routerCallback); + private native void nativeEnterRoom(long nativePointer, String rtpCapabilities, long peerConnectionFactoryPointer, PeerConnection.RTCConfiguration rtcConfiguration); private native void nativeCloseRoom(long nativePointer); private native void nativeCreateSendTransport(long nativeRoomPointer, String body); private native void nativeCreateRecvTransport(long nativeRoomPointer, String body); 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 109565f..7d70f93 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 @@ -4,8 +4,10 @@ import android.os.Handler; import android.util.Log; import com.acgist.taoyao.boot.model.Message; +import com.acgist.taoyao.boot.utils.ListUtils; import com.acgist.taoyao.boot.utils.MapUtils; import com.acgist.taoyao.media.MediaManager; +import com.acgist.taoyao.media.VideoSourceType; import com.acgist.taoyao.media.config.Config; import com.acgist.taoyao.media.signal.ITaoyao; @@ -74,7 +76,8 @@ public class SessionClient extends Client { return; } super.init(); - this.peerConnectionFactory = this.mediaManager.newClient(MediaManager.Type.BACK); + this.peerConnectionFactory = this.mediaManager.newClient(VideoSourceType.BACK); + this.mediaManager.startVideoCapture(); // STUN | TURN final List iceServers = new ArrayList<>(); // TODO:读取配置 @@ -86,6 +89,16 @@ public class SessionClient extends Client { this.sdpObserver = this.sdpObserver(); this.peerConnection = this.peerConnectionFactory.createPeerConnection(configuration, this.observer); this.peerConnection.addStream(this.mediaStream); + // TODO:连接streamId作用同步 +// final List streamIds = new ArrayList<>(); +// ListUtils.getOnlyOne(this.mediaStream.audioTracks, audioTrack -> { +// this.peerConnection.addTrack(audioTrack, streamIds); +// return audioTrack; +// }); +// ListUtils.getOnlyOne(this.mediaStream.videoTracks, videoTrack -> { +// this.peerConnection.addTrack(videoTrack, streamIds); +// return videoTrack; +// }); } } @@ -164,19 +177,28 @@ public class SessionClient extends Client { if(this.remoteMediaStream == null) { return; } - this.remoteMediaStream.audioTracks.forEach(v -> v.setEnabled(true)); + ListUtils.getOnlyOne(this.remoteMediaStream.audioTracks, audioTrack -> { + audioTrack.setEnabled(true); + return audioTrack; + }); } @Override public void pauseAudio() { super.pauseAudio(); - this.remoteMediaStream.audioTracks.forEach(v -> v.setEnabled(false)); + ListUtils.getOnlyOne(this.remoteMediaStream.audioTracks, audioTrack -> { + audioTrack.setEnabled(false); + return audioTrack; + }); } @Override public void resumeAudio() { super.resumeAudio(); - this.remoteMediaStream.audioTracks.forEach(v -> v.setEnabled(true)); + ListUtils.getOnlyOne(this.remoteMediaStream.audioTracks, audioTrack -> { + audioTrack.setEnabled(true); + return audioTrack; + }); } @Override @@ -185,24 +207,32 @@ public class SessionClient extends Client { if(this.remoteMediaStream == null) { return; } - final VideoTrack videoTrack = this.remoteMediaStream.videoTracks.get(0); - if(this.surfaceViewRenderer == null) { - this.surfaceViewRenderer = this.mediaManager.buildSurfaceViewRenderer(Config.WHAT_NEW_REMOTE_VIDEO, videoTrack); - } else { - videoTrack.setEnabled(true); - } + ListUtils.getOnlyOne(this.remoteMediaStream.videoTracks, videoTrack -> { + if(this.surfaceViewRenderer == null) { + this.surfaceViewRenderer = this.mediaManager.buildSurfaceViewRenderer(Config.WHAT_NEW_REMOTE_VIDEO, videoTrack); + } else { + videoTrack.setEnabled(true); + } + return videoTrack; + }); } @Override public void pauseVideo() { super.pauseVideo(); - this.mediaStream.videoTracks.forEach(v -> v.setEnabled(false)); + ListUtils.getOnlyOne(this.remoteMediaStream.videoTracks, videoTrack -> { + videoTrack.setEnabled(false); + return videoTrack; + }); } @Override public void resumeVideo() { super.resumeVideo(); - this.mediaStream.videoTracks.forEach(v -> v.setEnabled(true)); + ListUtils.getOnlyOne(this.remoteMediaStream.videoTracks, videoTrack -> { + videoTrack.setEnabled(true); + return videoTrack; + }); } @Override @@ -227,6 +257,7 @@ public class SessionClient extends Client { } super.close(); this.remoteMediaStream.dispose(); + this.mediaManager.stopVideoCapture(); this.mediaManager.closeClient(); } } @@ -294,8 +325,8 @@ public class SessionClient extends Client { public void onRenegotiationNeeded() { Log.d(SessionClient.class.getSimpleName(), "重新协商媒体:" + SessionClient.this.sessionId); if(peerConnection.connectionState() == PeerConnection.PeerConnectionState.CONNECTED) { - // TODO:重写协商 -// SessionClient.this.offer(); + // TODO:重新协商 +// SessionClient.this.offer(); } } diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/Config.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/Config.java index 6232f23..96738b8 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/Config.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/Config.java @@ -27,5 +27,9 @@ public class Config { * 新建远程视频 */ public static final int WHAT_NEW_REMOTE_VIDEO = 2003; + /** + * 默认声音大小 + */ + public static final double DEFAULT_VOLUME = 100.0D; } diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/WebrtcProperties.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/WebrtcProperties.java new file mode 100644 index 0000000..10ea40c --- /dev/null +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/WebrtcProperties.java @@ -0,0 +1,41 @@ +package com.acgist.taoyao.media.config; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * WebRTC配置 + * P2P视频监控会用,正常会议不会使用,需要自己搭建`coturn`服务。 + * + * @author acgist + */ +public class WebrtcProperties { + + private Boolean encrypt; + private WebrtcStunProperties[] stun; + private WebrtcTurnProperties[] turn; + + public List> getIceServers() { + final List> list = new ArrayList<>(); + if(this.stun != null) { + for (WebrtcStunProperties stun : this.stun) { + final Map map = new HashMap<>(); + map.put("urls", stun.getAddress()); + list.add(map); + } + } + if(this.turn != null) { + for (WebrtcTurnProperties turn : this.turn) { + final Map map = new HashMap<>(); + map.put("urls", turn.getAddress()); + map.put("username", turn.getUsername()); + map.put("credential", turn.getPassword()); + list.add(map); + } + } + return list; + } + +} diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/WebrtcStunProperties.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/WebrtcStunProperties.java new file mode 100644 index 0000000..075fdbe --- /dev/null +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/WebrtcStunProperties.java @@ -0,0 +1,33 @@ +package com.acgist.taoyao.media.config; + +/** + * WebRTC STUN配置 + * + * @author acgist + */ +public class WebrtcStunProperties { + + protected String host; + protected Integer port; + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getAddress() { + return "stun:" + this.host + ":" + this.port; + } + +} diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/WebrtcTurnProperties.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/WebrtcTurnProperties.java new file mode 100644 index 0000000..84eada7 --- /dev/null +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/WebrtcTurnProperties.java @@ -0,0 +1,33 @@ +package com.acgist.taoyao.media.config; + +/** + * WebRTC TURN配置 + * + * @author acgist + */ +public class WebrtcTurnProperties extends WebrtcStunProperties { + + private String username; + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getAddress() { + return "turn:" + this.host + ":" + this.port; + } + +} diff --git a/taoyao-client-web/src/components/Taoyao.js b/taoyao-client-web/src/components/Taoyao.js index e879c1b..d9bfcc0 100644 --- a/taoyao-client-web/src/components/Taoyao.js +++ b/taoyao-client-web/src/components/Taoyao.js @@ -1729,9 +1729,6 @@ class Taoyao extends RemoteClient { iceCandidates, dtlsParameters: { ...dtlsParameters, - // Remote DTLS role. We know it's always 'auto' by default so, if - // we want, we can force local WebRTC transport to be 'client' by - // indicating 'server' here and vice-versa. role: "auto", }, sctpParameters,