├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── libs │ └── google-webrtc-1.0.32006.aar ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── rd │ │ └── webrtctest │ │ ├── App.java │ │ ├── CameraUtil.java │ │ ├── MainActivity.java │ │ ├── ProxyVideoSink.java │ │ ├── SdpBean.java │ │ └── WebRtcUtil.java │ └── res │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ └── strings.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | *.aab 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | # Uncomment the following line in case you need and you don't have the release build type files in your app 17 | # release/ 18 | 19 | # Gradle files 20 | .gradle/ 21 | build/ 22 | 23 | # Local configuration file (sdk path, etc) 24 | local.properties 25 | 26 | # Proguard folder generated by Eclipse 27 | proguard/ 28 | 29 | # Log Files 30 | *.log 31 | 32 | # Android Studio Navigation editor temp files 33 | .navigation/ 34 | 35 | # Android Studio captures folder 36 | captures/ 37 | 38 | # IntelliJ 39 | *.iml 40 | .idea/workspace.xml 41 | .idea/tasks.xml 42 | .idea/gradle.xml 43 | .idea/assetWizardSettings.xml 44 | .idea/dictionaries 45 | .idea/libraries 46 | # Android Studio 3 in .gitignore file. 47 | .idea/ 48 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 49 | .idea/navEditor.xml 50 | 51 | 52 | # Keystore files 53 | # Uncomment the following lines if you do not want to check your keystore files in. 54 | #*.jks 55 | #*.keystore 56 | 57 | # External native build folder generated in Android Studio 2.2 and later 58 | .externalNativeBuild 59 | .cxx/ 60 | 61 | # Google Services (e.g. APIs or Firebase) 62 | # google-services.json 63 | 64 | # Freeline 65 | freeline.py 66 | freeline/ 67 | freeline_project_description.json 68 | 69 | # fastlane 70 | fastlane/report.xml 71 | fastlane/Preview.html 72 | fastlane/screenshots 73 | fastlane/test_output 74 | fastlane/readme.md 75 | 76 | # Version control 77 | vcs.xml 78 | 79 | # lint 80 | lint/intermediates/ 81 | lint/generated/ 82 | lint/outputs/ 83 | lint/tmp/ 84 | # lint/reports/ 85 | 86 | #去除打包apk的影响 87 | release/ 88 | debug/ 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android使用webrtc推拉流播放demo 2 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | compileSdkVersion 30 7 | buildToolsVersion "30.0.2" 8 | 9 | defaultConfig { 10 | applicationId "com.rd.webrtctest" 11 | minSdkVersion 23 12 | targetSdkVersion 30 13 | versionCode 1 14 | versionName "1.0" 15 | ndk { 16 | abiFilters "armeabi-v7a" 17 | // 如果您使用的是商业版,只能使用 armeabi 架构,即: 18 | // abiFilters "armeabi", 19 | } 20 | javaCompileOptions { 21 | annotationProcessorOptions { 22 | arguments = [ 23 | //Pass in RxJava version, can pass in RxJava2, RxJava3 24 | rxhttp_rxjava: 'rxjava3', 25 | rxhttp_package: 'rxhttp', //Specifies the RxHttp class package 26 | AROUTER_MODULE_NAME: project.getName() 27 | ] 28 | } 29 | } 30 | 31 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 32 | } 33 | 34 | buildTypes { 35 | release { 36 | minifyEnabled false 37 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 38 | } 39 | } 40 | sourceSets { 41 | main { 42 | jniLibs.srcDirs = ['libs'] 43 | } 44 | } 45 | repositories { 46 | flatDir { dirs 'libs' } 47 | } 48 | compileOptions { 49 | sourceCompatibility JavaVersion.VERSION_1_8 50 | targetCompatibility JavaVersion.VERSION_1_8 51 | } 52 | } 53 | 54 | dependencies { 55 | 56 | implementation 'androidx.appcompat:appcompat:1.2.0' 57 | implementation 'com.google.android.material:material:1.2.1' 58 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 59 | // implementation project(path: ':libwebrtc') 60 | implementation(name: 'google-webrtc-1.0.32006', ext: 'aar') 61 | testImplementation 'junit:junit:4.+' 62 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 63 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 64 | 65 | implementation 'com.hjq:xxpermissions:10.6' 66 | 67 | implementation 'com.ljx.rxhttp:rxhttp:2.5.7' 68 | implementation 'com.squareup.okhttp3:okhttp:4.9.1' 69 | annotationProcessor 'com.ljx.rxhttp:rxhttp-compiler:2.5.7' //生成RxHttp类,纯Java项目,请使用annotationProcessor代替kapt 70 | //rxjava3 71 | implementation 'io.reactivex.rxjava3:rxjava:3.0.6' 72 | implementation 'io.reactivex.rxjava3:rxandroid:3.0.0' 73 | implementation 'com.ljx.rxlife3:rxlife-rxjava:3.0.0' //管理RxJava3生命周期,页面销毁,关闭请求 74 | 75 | //lombok 76 | compileOnly 'org.projectlombok:lombok:1.18.16' 77 | annotationProcessor 'org.projectlombok:lombok:1.18.16' 78 | //gson 79 | implementation 'com.google.code.gson:gson:2.8.6' 80 | } -------------------------------------------------------------------------------- /app/libs/google-webrtc-1.0.32006.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Henry-6/WebRTCTest/7cde20dee1fc02db1a454b4085d81a22edd4a683/app/libs/google-webrtc-1.0.32006.aar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/rd/webrtctest/App.java: -------------------------------------------------------------------------------- 1 | package com.rd.webrtctest; 2 | 3 | import android.app.Application; 4 | 5 | import rxhttp.RxHttp; 6 | 7 | /** 8 | * @author haimian on 2021/4/22 0022 9 | */ 10 | public class App extends Application { 11 | 12 | 13 | @Override 14 | public void onCreate() { 15 | super.onCreate(); 16 | 17 | 18 | RxHttp.setDebug(false); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/rd/webrtctest/CameraUtil.java: -------------------------------------------------------------------------------- 1 | package com.rd.webrtctest; 2 | 3 | import android.content.Context; 4 | 5 | import org.webrtc.Camera1Enumerator; 6 | import org.webrtc.Camera2Enumerator; 7 | import org.webrtc.CameraEnumerator; 8 | import org.webrtc.VideoCapturer; 9 | 10 | /** 11 | * @author haimian on 2021/12/16 0016 12 | */ 13 | public class CameraUtil { 14 | 15 | /** 16 | * 创建媒体方式 17 | * 18 | * @return VideoCapturer 19 | */ 20 | public static VideoCapturer createVideoCapture(Context context) { 21 | VideoCapturer videoCapturer; 22 | if (Camera2Enumerator.isSupported(context)) { 23 | videoCapturer = createCameraCapture(new Camera2Enumerator(context)); 24 | } else { 25 | videoCapturer = createCameraCapture(new Camera1Enumerator(true)); 26 | } 27 | return videoCapturer; 28 | } 29 | 30 | /** 31 | * 创建相机媒体流 32 | */ 33 | private static VideoCapturer createCameraCapture(CameraEnumerator enumerator) { 34 | final String[] deviceNames = enumerator.getDeviceNames(); 35 | 36 | // First, try to find front facing camera 37 | for (String deviceName : deviceNames) { 38 | if (enumerator.isFrontFacing(deviceName)) { 39 | VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null); 40 | if (videoCapturer != null) { 41 | return videoCapturer; 42 | } 43 | } 44 | } 45 | 46 | // Front facing camera not found, try something else 47 | for (String deviceName : deviceNames) { 48 | if (!enumerator.isFrontFacing(deviceName)) { 49 | VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null); 50 | if (videoCapturer != null) { 51 | return videoCapturer; 52 | } 53 | } 54 | } 55 | return null; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/rd/webrtctest/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.rd.webrtctest; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | 5 | import android.os.Bundle; 6 | import android.text.TextUtils; 7 | import android.view.View; 8 | import android.widget.EditText; 9 | import android.widget.RadioButton; 10 | import android.widget.Toast; 11 | 12 | import com.hjq.permissions.OnPermissionCallback; 13 | import com.hjq.permissions.Permission; 14 | import com.hjq.permissions.XXPermissions; 15 | 16 | import org.webrtc.EglBase; 17 | import org.webrtc.PeerConnectionFactory; 18 | import org.webrtc.RendererCommon; 19 | import org.webrtc.SurfaceViewRenderer; 20 | import java.util.List; 21 | 22 | 23 | public class MainActivity extends AppCompatActivity { 24 | 25 | private SurfaceViewRenderer surfaceViewRenderer1; 26 | private SurfaceViewRenderer surfaceViewRenderer2; 27 | 28 | private EditText ed1; 29 | private EditText ed2; 30 | private RadioButton rbPushAudio; 31 | private RadioButton rbPushVideo; 32 | private RadioButton rbPlayVideo; 33 | private RadioButton rbPlayAudio; 34 | private EglBase mRootEglBase; 35 | 36 | @Override 37 | protected void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.activity_main); 40 | 41 | surfaceViewRenderer1 = findViewById(R.id.activity_main_svr_video); 42 | surfaceViewRenderer2 = findViewById(R.id.activity_main_svr_video1); 43 | ed1 = findViewById(R.id.ed1); 44 | ed2 = findViewById(R.id.ed2); 45 | long time = System.currentTimeMillis() / 1000; 46 | ed1.setText("https://zlv.runde.pro/index/api/webrtc?app=live&stream=" + time + "&type=push"); 47 | ed2.setText("https://zlv.runde.pro/index/api/webrtc?app=live&stream=" + time + "&type=play"); 48 | rbPushAudio = findViewById(R.id.rbPushAudio); 49 | rbPushVideo = findViewById(R.id.rbPushVideo); 50 | rbPlayVideo = findViewById(R.id.rbPlayVideo); 51 | rbPlayAudio = findViewById(R.id.rbPlayAudio); 52 | mRootEglBase = EglBase.create(); 53 | 54 | //初始化SurfaceViewRenderer 55 | surfaceViewRenderer1.init(mRootEglBase.getEglBaseContext(), new RendererCommon.RendererEvents() { 56 | @Override 57 | public void onFirstFrameRendered() { 58 | 59 | } 60 | 61 | @Override 62 | public void onFrameResolutionChanged(int videoWidth, int videoHeight, int rotation) { 63 | 64 | } 65 | }); 66 | surfaceViewRenderer1.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL); 67 | // surfaceViewRenderer.setMirror(false); 68 | surfaceViewRenderer1.setEnableHardwareScaler(true); 69 | surfaceViewRenderer1.setZOrderMediaOverlay(true); 70 | 71 | surfaceViewRenderer2.init(mRootEglBase.getEglBaseContext(), new RendererCommon.RendererEvents() { 72 | @Override 73 | public void onFirstFrameRendered() { 74 | 75 | } 76 | 77 | @Override 78 | public void onFrameResolutionChanged(int videoWidth, int videoHeight, int rotation) { 79 | 80 | } 81 | }); 82 | 83 | surfaceViewRenderer2.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL); 84 | // surfaceViewRenderer1.setMirror(true); 85 | surfaceViewRenderer2.setEnableHardwareScaler(true); 86 | surfaceViewRenderer2.setZOrderMediaOverlay(true); 87 | 88 | XXPermissions.with(this) 89 | .permission(Permission.CAMERA) 90 | .permission(Permission.RECORD_AUDIO) 91 | .request(new OnPermissionCallback() { 92 | @Override 93 | public void onGranted(List permissions, boolean all) { 94 | } 95 | }); 96 | } 97 | 98 | @Override 99 | protected void onDestroy() { 100 | super.onDestroy(); 101 | surfaceViewRenderer1.release(); 102 | surfaceViewRenderer2.release(); 103 | PeerConnectionFactory.stopInternalTracingCapture(); 104 | PeerConnectionFactory.shutdownInternalTracer(); 105 | } 106 | 107 | public void doPush(View view) { 108 | doPush(); 109 | } 110 | 111 | private WebRtcUtil webRtcUtil1; 112 | 113 | private void doPush() { 114 | String text = ed1.getEditableText().toString(); 115 | if (TextUtils.isEmpty(text)) { 116 | Toast.makeText(MainActivity.this, "推流地址为空!", Toast.LENGTH_SHORT).show(); 117 | return; 118 | } 119 | if (webRtcUtil1 != null) { 120 | webRtcUtil1.destroy(); 121 | } 122 | webRtcUtil1 = new WebRtcUtil(MainActivity.this); 123 | webRtcUtil1.create(mRootEglBase, rbPushVideo.isChecked() ? surfaceViewRenderer1 : null, true, rbPushVideo.isChecked(), text, new WebRtcUtil.WebRtcCallBack(){ 124 | @Override 125 | public void onSuccess() { 126 | 127 | } 128 | 129 | @Override 130 | public void onFail() { 131 | 132 | } 133 | }); 134 | } 135 | 136 | public void doPlay(View view) { 137 | doPlay(); 138 | } 139 | 140 | private WebRtcUtil webRtcUtil2; 141 | 142 | private void doPlay() { 143 | String text = ed2.getEditableText().toString(); 144 | if (TextUtils.isEmpty(text)) { 145 | Toast.makeText(MainActivity.this, "拉流地址为空!", Toast.LENGTH_SHORT).show(); 146 | return; 147 | } 148 | if (webRtcUtil2 != null) { 149 | webRtcUtil2.destroy(); 150 | } 151 | webRtcUtil2 = new WebRtcUtil(MainActivity.this); 152 | webRtcUtil2.create(mRootEglBase, rbPlayVideo.isChecked() ? surfaceViewRenderer2 : null, false, rbPlayVideo.isChecked(), text, new WebRtcUtil.WebRtcCallBack(){ 153 | @Override 154 | public void onSuccess() { 155 | 156 | } 157 | 158 | @Override 159 | public void onFail() { 160 | 161 | } 162 | }); 163 | } 164 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rd/webrtctest/ProxyVideoSink.java: -------------------------------------------------------------------------------- 1 | package com.rd.webrtctest; 2 | 3 | import org.webrtc.Logging; 4 | import org.webrtc.VideoFrame; 5 | import org.webrtc.VideoSink; 6 | 7 | /** 8 | * Created by dds on 2019/4/4. 9 | * android_shuai@163.com 10 | */ 11 | public class ProxyVideoSink implements VideoSink { 12 | private static final String TAG = "dds_ProxyVideoSink"; 13 | private VideoSink target; 14 | 15 | @Override 16 | synchronized public void onFrame(VideoFrame frame) { 17 | if (target == null) { 18 | Logging.d(TAG, "Dropping frame in proxy because target is null."); 19 | return; 20 | } 21 | target.onFrame(frame); 22 | } 23 | 24 | synchronized public void setTarget(VideoSink target) { 25 | this.target = target; 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rd/webrtctest/SdpBean.java: -------------------------------------------------------------------------------- 1 | package com.rd.webrtctest; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | /** 6 | * @author haimian on 2021/4/22 0022 7 | */ 8 | @lombok.NoArgsConstructor 9 | @lombok.Data 10 | public class SdpBean { 11 | /** 12 | * code : 0 13 | * server : vid-4416-674 14 | * sdp : v=0 15 | o=SRS/4.0.89(Leo) 54934496 2 IN IP4 0.0.0.0 16 | s=SRSPlaySession 17 | t=0 0 18 | a=ice-lite 19 | a=group:BUNDLE 0 1 20 | a=msid-semantic: WMS live/36147_1010 21 | m=audio 9 UDP/TLS/RTP/SAVPF 111 22 | c=IN IP4 0.0.0.0 23 | a=ice-ufrag:85beu046 24 | a=ice-pwd:7cc955e059om1u6g089764598h0e96d2 25 | a=fingerprint:sha-256 8F:C4:D7:B1:CA:3E:62:57:A1:14:8C:B3:5F:EA:46:2D:32:B0:BB:B7:70:45:F1:3C:0E:AA:AF:17:CD:9F:37:56 26 | a=setup:passive 27 | a=mid:0 28 | a=sendonly 29 | a=rtcp-mux 30 | a=rtcp-rsize 31 | a=rtpmap:111 opus/48000/2 32 | a=ssrc:44606871 cname:56l22i98346r576j 33 | a=ssrc:44606871 label:audio-6v46lg11 34 | a=candidate:0 1 udp 2130706431 47.105.215.67 40500 typ host generation 0 35 | m=video 9 UDP/TLS/RTP/SAVPF 125 36 | c=IN IP4 0.0.0.0 37 | a=ice-ufrag:85beu046 38 | a=ice-pwd:7cc955e059om1u6g089764598h0e96d2 39 | a=fingerprint:sha-256 8F:C4:D7:B1:CA:3E:62:57:A1:14:8C:B3:5F:EA:46:2D:32:B0:BB:B7:70:45:F1:3C:0E:AA:AF:17:CD:9F:37:56 40 | a=setup:passive 41 | a=mid:1 42 | a=sendonly 43 | a=rtcp-mux 44 | a=rtcp-rsize 45 | a=rtpmap:125 H264/90000 46 | a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f 47 | a=ssrc:44606872 cname:56l22i98346r576j 48 | a=ssrc:44606872 label:video-6179ht01 49 | a=candidate:0 1 udp 2130706431 47.105.215.67 40500 typ host generation 0 50 | * sessionid : 85beu046:Rlqi 51 | */ 52 | 53 | @SerializedName("code") 54 | private int code; 55 | @SerializedName("server") 56 | private String server; 57 | @SerializedName("sdp") 58 | private String sdp; 59 | @SerializedName("sessionid") 60 | private String sessionid; 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/rd/webrtctest/WebRtcUtil.java: -------------------------------------------------------------------------------- 1 | package com.rd.webrtctest; 2 | 3 | import android.content.Context; 4 | import android.text.TextUtils; 5 | import android.util.Log; 6 | 7 | import com.google.gson.Gson; 8 | 9 | import org.webrtc.AudioSource; 10 | import org.webrtc.AudioTrack; 11 | import org.webrtc.DataChannel; 12 | import org.webrtc.DefaultVideoDecoderFactory; 13 | import org.webrtc.DefaultVideoEncoderFactory; 14 | import org.webrtc.EglBase; 15 | import org.webrtc.IceCandidate; 16 | import org.webrtc.Logging; 17 | import org.webrtc.MediaConstraints; 18 | import org.webrtc.MediaStream; 19 | import org.webrtc.MediaStreamTrack; 20 | import org.webrtc.PeerConnection; 21 | import org.webrtc.PeerConnectionFactory; 22 | import org.webrtc.RtpReceiver; 23 | import org.webrtc.RtpSender; 24 | import org.webrtc.RtpTransceiver; 25 | import org.webrtc.SdpObserver; 26 | import org.webrtc.SessionDescription; 27 | import org.webrtc.SurfaceTextureHelper; 28 | import org.webrtc.SurfaceViewRenderer; 29 | import org.webrtc.VideoCapturer; 30 | import org.webrtc.VideoDecoderFactory; 31 | import org.webrtc.VideoEncoderFactory; 32 | import org.webrtc.VideoSource; 33 | import org.webrtc.VideoTrack; 34 | import org.webrtc.audio.JavaAudioDeviceModule; 35 | import org.webrtc.voiceengine.WebRtcAudioUtils; 36 | 37 | import java.util.ArrayList; 38 | 39 | import rxhttp.RxHttp; 40 | 41 | 42 | /** 43 | * @author haimian on 2021/4/24 0024 44 | */ 45 | public class WebRtcUtil implements PeerConnection.Observer, SdpObserver { 46 | 47 | private Context context; 48 | 49 | public WebRtcUtil(Context context){ 50 | this.context = context.getApplicationContext(); 51 | } 52 | 53 | private EglBase eglBase; 54 | 55 | private String playUrl; 56 | 57 | private PeerConnection peerConnection; 58 | private SurfaceViewRenderer surfaceViewRenderer; 59 | private PeerConnectionFactory peerConnectionFactory; 60 | 61 | private AudioSource audioSource; 62 | private VideoSource videoSource; 63 | private AudioTrack localAudioTrack; 64 | private VideoTrack localVideoTrack; 65 | private VideoCapturer captureAndroid; 66 | private SurfaceTextureHelper surfaceTextureHelper; 67 | public static final String VIDEO_TRACK_ID = "ARDAMSv0"; 68 | public static final String AUDIO_TRACK_ID = "ARDAMSa0"; 69 | private boolean isShowCamera = false; 70 | private static final int VIDEO_RESOLUTION_WIDTH = 1280; 71 | private static final int VIDEO_RESOLUTION_HEIGHT = 720; 72 | private static final int FPS = 30; 73 | /** 74 | * isPublish true为推流 false为拉流 75 | */ 76 | private boolean isPublish; 77 | 78 | public void create(EglBase eglBase, SurfaceViewRenderer surfaceViewRenderer, String playUrl, WebRtcCallBack callBack) { 79 | create(eglBase, surfaceViewRenderer, false, playUrl, callBack); 80 | } 81 | 82 | public void create(EglBase eglBase, SurfaceViewRenderer surfaceViewRenderer, boolean isPublish, String playUrl, WebRtcCallBack callBack) { 83 | this.eglBase = eglBase; 84 | this.surfaceViewRenderer = surfaceViewRenderer; 85 | this.callBack = callBack; 86 | this.playUrl = playUrl; 87 | this.isPublish = isPublish; 88 | 89 | init(); 90 | } 91 | 92 | public void create(EglBase eglBase, SurfaceViewRenderer surfaceViewRenderer, boolean isPublish, boolean isShowCamera, String playUrl, WebRtcCallBack callBack) { 93 | this.eglBase = eglBase; 94 | this.surfaceViewRenderer = surfaceViewRenderer; 95 | this.callBack = callBack; 96 | this.playUrl = playUrl; 97 | this.isPublish = isPublish; 98 | this.isShowCamera = isShowCamera; 99 | 100 | init(); 101 | } 102 | 103 | private void init() { 104 | peerConnectionFactory = getPeerConnectionFactory(context); 105 | // NOTE: this _must_ happen while PeerConnectionFactory is alive! 106 | Logging.enableLogToDebugOutput(Logging.Severity.LS_NONE); 107 | 108 | peerConnection = peerConnectionFactory.createPeerConnection(getConfig(), this); 109 | MediaConstraints mediaConstraints = new MediaConstraints(); 110 | 111 | if (!isPublish) { 112 | //设置仅接收音视频 113 | peerConnection.addTransceiver(MediaStreamTrack.MediaType.MEDIA_TYPE_AUDIO, new RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.RECV_ONLY)); 114 | peerConnection.addTransceiver(MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO, new RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.RECV_ONLY)); 115 | } 116 | else { 117 | //设置仅推送音视频 118 | peerConnection.addTransceiver(MediaStreamTrack.MediaType.MEDIA_TYPE_AUDIO, new RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.SEND_ONLY)); 119 | peerConnection.addTransceiver(MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO, new RtpTransceiver.RtpTransceiverInit(RtpTransceiver.RtpTransceiverDirection.SEND_ONLY)); 120 | 121 | //设置回声去噪 122 | WebRtcAudioUtils.setWebRtcBasedAcousticEchoCanceler(true); 123 | WebRtcAudioUtils.setWebRtcBasedNoiseSuppressor(true); 124 | 125 | // 音频 126 | audioSource = peerConnectionFactory.createAudioSource(createAudioConstraints()); 127 | localAudioTrack = peerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource); 128 | localAudioTrack.setEnabled(true); 129 | 130 | peerConnection.addTrack(localAudioTrack); 131 | //是否显示摄像头画面 132 | if (isShowCamera) { 133 | captureAndroid = CameraUtil.createVideoCapture(context); 134 | surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBase.getEglBaseContext()); 135 | 136 | videoSource = peerConnectionFactory.createVideoSource(false); 137 | 138 | captureAndroid.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver()); 139 | captureAndroid.startCapture(VIDEO_RESOLUTION_WIDTH, VIDEO_RESOLUTION_HEIGHT, FPS); 140 | 141 | localVideoTrack = peerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource); 142 | localVideoTrack.setEnabled(true); 143 | if (surfaceViewRenderer != null) { 144 | ProxyVideoSink videoSink = new ProxyVideoSink(); 145 | videoSink.setTarget(surfaceViewRenderer); 146 | localVideoTrack.addSink(videoSink); 147 | } 148 | peerConnection.addTrack(localVideoTrack); 149 | } 150 | } 151 | peerConnection.createOffer(this, mediaConstraints); 152 | } 153 | 154 | public void destroy() { 155 | if (callBack != null) { 156 | callBack = null; 157 | } 158 | if (peerConnection != null) { 159 | peerConnection.dispose(); 160 | peerConnection = null; 161 | } 162 | if (surfaceTextureHelper != null) { 163 | surfaceTextureHelper.dispose(); 164 | surfaceTextureHelper = null; 165 | } 166 | if (captureAndroid != null) { 167 | captureAndroid.dispose(); 168 | captureAndroid = null; 169 | } 170 | if (surfaceViewRenderer != null) { 171 | surfaceViewRenderer.clearImage(); 172 | } 173 | if (peerConnectionFactory != null) { 174 | peerConnectionFactory.dispose(); 175 | peerConnectionFactory = null; 176 | } 177 | } 178 | 179 | private static final String AUDIO_ECHO_CANCELLATION_CONSTRAINT = "googEchoCancellation"; 180 | private static final String AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT = "googAutoGainControl"; 181 | private static final String AUDIO_HIGH_PASS_FILTER_CONSTRAINT = "googHighpassFilter"; 182 | private static final String AUDIO_NOISE_SUPPRESSION_CONSTRAINT = "googNoiseSuppression"; 183 | 184 | /** 185 | * 配置音频参数 186 | * @return 187 | */ 188 | private MediaConstraints createAudioConstraints() { 189 | MediaConstraints audioConstraints = new MediaConstraints(); 190 | audioConstraints.mandatory.add( 191 | new MediaConstraints.KeyValuePair(AUDIO_ECHO_CANCELLATION_CONSTRAINT, "true")); 192 | audioConstraints.mandatory.add( 193 | new MediaConstraints.KeyValuePair(AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT, "false")); 194 | audioConstraints.mandatory.add( 195 | new MediaConstraints.KeyValuePair(AUDIO_HIGH_PASS_FILTER_CONSTRAINT, "false")); 196 | audioConstraints.mandatory.add( 197 | new MediaConstraints.KeyValuePair(AUDIO_NOISE_SUPPRESSION_CONSTRAINT, "true")); 198 | return audioConstraints; 199 | } 200 | 201 | 202 | private int reConnCount; 203 | private final int MAX_CONN_COUNT = 10; 204 | 205 | public void openWebRtc(String sdp) { 206 | //isPublish true时xxxx的url后缀应为publish false时xxxx的url后缀为play 207 | //例: "https://www.baidu.com/rtc/v1/publish" : "https://www.baidu.com/rtc/v1/play" 208 | //请求的url和api的参数为同一个内容 209 | RxHttp.postBody(playUrl, sdp) 210 | // .add("app", "live") 211 | // .add("stream", "test") 212 | // .add("type", isPublish ? "push" : "play") 213 | .setBody(sdp, null) 214 | .asString() 215 | .subscribe(s -> { 216 | s = s.replaceAll("\n", ""); 217 | Log.e("WebRtc流", "是否推流: " + isPublish + " 地址:" + playUrl + s); 218 | if (!TextUtils.isEmpty(s)) { 219 | SdpBean sdpBean = new Gson().fromJson(s, SdpBean.class); 220 | if (sdpBean.getCode() == 400) { 221 | openWebRtc(sdp); 222 | return; 223 | } 224 | if (!TextUtils.isEmpty(sdpBean.getSdp())) { 225 | setRemoteSdp(sdpBean.getSdp()); 226 | } 227 | } 228 | }, throwable -> { 229 | openWebRtc(sdp); 230 | }); 231 | } 232 | 233 | public void setRemoteSdp(String sdp) { 234 | if (peerConnection != null) { 235 | SessionDescription remoteSpd = new SessionDescription(SessionDescription.Type.ANSWER, sdp); 236 | peerConnection.setRemoteDescription(this, remoteSpd); 237 | } 238 | } 239 | 240 | public interface WebRtcCallBack { 241 | void onSuccess(); 242 | void onFail(); 243 | } 244 | 245 | private WebRtcCallBack callBack; 246 | 247 | /** 248 | * 获取 PeerConnectionFactory 249 | */ 250 | private PeerConnectionFactory getPeerConnectionFactory(Context context) { 251 | PeerConnectionFactory.InitializationOptions initializationOptions = PeerConnectionFactory.InitializationOptions.builder(context) 252 | .setEnableInternalTracer(true) 253 | .setFieldTrials("WebRTC-H264HighProfile/Enabled/") 254 | .createInitializationOptions(); 255 | 256 | PeerConnectionFactory.initialize(initializationOptions); 257 | 258 | // 2. 设置编解码方式:默认方法 259 | VideoEncoderFactory encoderFactory = new DefaultVideoEncoderFactory( 260 | eglBase.getEglBaseContext(), 261 | false, 262 | true); 263 | VideoDecoderFactory decoderFactory = new DefaultVideoDecoderFactory(eglBase.getEglBaseContext()); 264 | 265 | // 构造Factory 266 | PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions 267 | .builder(context) 268 | .createInitializationOptions()); 269 | 270 | return PeerConnectionFactory.builder() 271 | .setOptions(new PeerConnectionFactory.Options()) 272 | .setAudioDeviceModule(JavaAudioDeviceModule.builder(context).createAudioDeviceModule()) 273 | .setVideoEncoderFactory(encoderFactory) 274 | .setVideoDecoderFactory(decoderFactory) 275 | .createPeerConnectionFactory(); 276 | } 277 | 278 | private PeerConnection.RTCConfiguration getConfig() { 279 | PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(new ArrayList<>()); 280 | //关闭分辨率变换 281 | rtcConfig.enableCpuOveruseDetection = false; 282 | //修改模式 PlanB无法使用仅接收音视频的配置 283 | rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN; 284 | return rtcConfig; 285 | } 286 | 287 | @Override 288 | public void onCreateSuccess(SessionDescription sdp) { 289 | if (sdp.type == SessionDescription.Type.OFFER) { 290 | //设置setLocalDescription offer返回sdp 291 | peerConnection.setLocalDescription(this, sdp); 292 | if (!TextUtils.isEmpty(sdp.description)) { 293 | reConnCount = 0; 294 | openWebRtc(sdp.description); 295 | } 296 | } 297 | } 298 | 299 | @Override 300 | public void onSetSuccess() { 301 | 302 | } 303 | 304 | @Override 305 | public void onCreateFailure(String error) { 306 | 307 | } 308 | 309 | @Override 310 | public void onSetFailure(String error) { 311 | 312 | } 313 | 314 | @Override 315 | public void onSignalingChange(PeerConnection.SignalingState newState) { 316 | 317 | } 318 | 319 | @Override 320 | public void onIceConnectionChange(PeerConnection.IceConnectionState newState) { 321 | 322 | } 323 | 324 | @Override 325 | public void onIceConnectionReceivingChange(boolean receiving) { 326 | 327 | } 328 | 329 | @Override 330 | public void onIceGatheringChange(PeerConnection.IceGatheringState newState) { 331 | 332 | } 333 | 334 | @Override 335 | public void onIceCandidate(IceCandidate candidate) { 336 | peerConnection.addIceCandidate(candidate); 337 | } 338 | 339 | @Override 340 | public void onIceCandidatesRemoved(IceCandidate[] candidates) { 341 | peerConnection.removeIceCandidates(candidates); 342 | } 343 | 344 | @Override 345 | public void onAddStream(MediaStream stream) { 346 | 347 | } 348 | 349 | @Override 350 | public void onRemoveStream(MediaStream stream) { 351 | 352 | } 353 | 354 | @Override 355 | public void onDataChannel(DataChannel dataChannel) { 356 | 357 | } 358 | 359 | @Override 360 | public void onRenegotiationNeeded() { 361 | 362 | } 363 | 364 | @Override 365 | public void onAddTrack(RtpReceiver receiver, MediaStream[] mediaStreams) { 366 | MediaStreamTrack track = receiver.track(); 367 | if (track instanceof VideoTrack) { 368 | VideoTrack remoteVideoTrack = (VideoTrack) track; 369 | remoteVideoTrack.setEnabled(true); 370 | if (surfaceViewRenderer != null && isShowCamera) { 371 | ProxyVideoSink videoSink = new ProxyVideoSink(); 372 | videoSink.setTarget(surfaceViewRenderer); 373 | remoteVideoTrack.addSink(videoSink); 374 | } 375 | } 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 22 | 23 | 28 | 29 | 35 | 36 | 37 |