From dd104a78f70f3fb76a1a80ad8a463ba3a535d4fd Mon Sep 17 00:00:00 2001 From: "frankxikun.yang" Date: Tue, 12 Aug 2025 21:06:10 +0800 Subject: [PATCH] feat: support RTM Pullstream for Android/iOS --- Android/APIExample/app/build.gradle | 9 +- .../examples/PullStreamActivity.java | 67 ++++++----- .../main/res/layout/activity_pull_stream.xml | 37 +++++-- Android/APIExample/settings.gradle | 10 ++ .../PullRTMP/PullRTMPViewController.swift | 104 +++++++++++++++--- iOS/ApiExample/Podfile | 4 +- 6 files changed, 173 insertions(+), 58 deletions(-) diff --git a/Android/APIExample/app/build.gradle b/Android/APIExample/app/build.gradle index 4c1eeaa7..e4a03be0 100644 --- a/Android/APIExample/app/build.gradle +++ b/Android/APIExample/app/build.gradle @@ -71,6 +71,10 @@ android { pickFirst 'lib/x86_64/libbytertc_vp8codec_extension.so' pickFirst 'lib/x86_64/libh265enc.so' + + // 删除重复的 libc++ 共享库 + pickFirst 'lib/arm64-v8a/libc++_shared.so' + pickFirst 'lib/armeabi-v7a/libc++_shared.so' } @@ -92,10 +96,9 @@ dependencies { implementation files('libs/ByteEffect.aar') implementation'com.faceunity:core:8.7.0' implementation'com.faceunity:model:8.7.0' - implementation "com.bytedanceapi:ttsdk-player_premium:1.40.2.8" + implementation 'com.bytedanceapi:ttsdk-player_premium:1.47.1.10' + implementation 'com.bytedanceapi:ttsdk-ttlivepull_rtc:1.47.1.10' - implementation ("com.bytedanceapi:ttsdk-ttlivepull:1.40.2.8") { - } implementation 'commons-net:commons-net:3.6' // 日志上报 SDK,用于点播日志上传, 使用 6.14.3 及以上版本 diff --git a/Android/APIExample/app/src/main/java/rtc/volcengine/apiexample/examples/PullStreamActivity.java b/Android/APIExample/app/src/main/java/rtc/volcengine/apiexample/examples/PullStreamActivity.java index 19501a22..c78621c5 100644 --- a/Android/APIExample/app/src/main/java/rtc/volcengine/apiexample/examples/PullStreamActivity.java +++ b/Android/APIExample/app/src/main/java/rtc/volcengine/apiexample/examples/PullStreamActivity.java @@ -6,22 +6,19 @@ import android.os.Bundle; import android.text.TextUtils; import android.util.Log; -import android.view.Surface; import android.view.SurfaceView; -import android.widget.Button; import android.widget.EditText; +import android.widget.RadioButton; +import android.widget.RadioGroup; import android.widget.TextView; import com.bytedance.vcloud.cacheModule.utils.CmLog; import com.pandora.common.env.Env; import com.pandora.common.env.config.Config; import com.pandora.common.env.config.VodConfig; -import com.pandora.live.player.LivePlayerBuilder; import com.pandora.ttlicense2.LicenseManager; import com.ss.mediakit.medialoader.AVMDLLog; import com.ss.ttvideoengine.utils.TTVideoEngineLog; -import com.ss.videoarch.liveplayer.ILiveListener; -import com.ss.videoarch.liveplayer.INetworkClient; import com.ss.videoarch.liveplayer.VeLivePlayer; import com.ss.videoarch.liveplayer.VeLivePlayerAudioFrame; import com.ss.videoarch.liveplayer.VeLivePlayerConfiguration; @@ -32,13 +29,8 @@ import com.ss.videoarch.liveplayer.VeLivePlayerStreamData; import com.ss.videoarch.liveplayer.VeLivePlayerVideoFrame; import com.ss.videoarch.liveplayer.VideoLiveManager; -import com.ss.videoarch.liveplayer.log.LiveError; -import com.ss.videoarch.liveplayer.log.VeLivePlayerLog; - -import org.json.JSONObject; import java.io.File; -import java.nio.ByteBuffer; import java.util.ArrayList; import rtc.volcengine.apiexample.BaseActivity; @@ -58,6 +50,10 @@ public class PullStreamActivity extends BaseActivity { private TextView seiMsg; private VeLivePlayer livePlayer; + private VeLivePlayerDef.VeLivePlayerFormat streamFormat; // 选中的是 FLV 拉流还是 RTM 拉流 + private VeLivePlayerDef.VeLivePlayerProtocol streamProtocol; // 使用的拉流协议 + + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -78,9 +74,6 @@ protected void onCreate(Bundle savedInstanceState) { // 配置播放器回调 livePlayer.setObserver(mLivePlayerObserver); - - - } // VeLivePlayerObserver 回调 @@ -183,6 +176,27 @@ private void initUI() { surfaceView = findViewById(R.id.video_view); livePlayer.setSurfaceHolder(surfaceView.getHolder()); + RadioGroup radioGroup = findViewById(R.id.streamTypeGroup); + + // 默认选择 FLV 拉流 + radioGroup.check(R.id.flvStreamType); + streamFormat = VeLivePlayerDef.VeLivePlayerFormat.VeLivePlayerFormatFLV; + streamProtocol = VeLivePlayerDef.VeLivePlayerProtocol.VeLivePlayerProtocolTCP; + + // 设置监听器 + radioGroup.setOnCheckedChangeListener((group, checkedId) -> { + switch (checkedId) { + case R.id.flvStreamType: + streamFormat = VeLivePlayerDef.VeLivePlayerFormat.VeLivePlayerFormatFLV; + streamProtocol = VeLivePlayerDef.VeLivePlayerProtocol.VeLivePlayerProtocolTCP; + break; + case R.id.rtmStreamType: + streamFormat = VeLivePlayerDef.VeLivePlayerFormat.VeLivePlayerFormatRTM; + streamProtocol = VeLivePlayerDef.VeLivePlayerProtocol.VeLivePlayerProtocolTLS; + break; + } + }); + btnStartPull = findViewById(R.id.btn_start_pull); btnStopPull = findViewById(R.id.btn_stop_pull); urlInput = findViewById(R.id.url_input); @@ -195,32 +209,29 @@ private void initUI() { return; } - // 配置 RTM 地址 - VeLivePlayerStreamData.VeLivePlayerStream playStreamRTM = new VeLivePlayerStreamData.VeLivePlayerStream(); - playStreamRTM.url = url; - playStreamRTM.format = VeLivePlayerDef.VeLivePlayerFormat.VeLivePlayerFormatFLV; - playStreamRTM.resolution = VeLivePlayerDef.VeLivePlayerResolution.VeLivePlayerResolutionOrigin; - playStreamRTM.streamType = VeLivePlayerDef.VeLivePlayerStreamType.VeLivePlayerStreamTypeMain; + // 配置流信息 + VeLivePlayerStreamData.VeLivePlayerStream playStream = new VeLivePlayerStreamData.VeLivePlayerStream(); + playStream.url = url; + playStream.format = streamFormat; + playStream.resolution = new VeLivePlayerDef.VeLivePlayerResolution(VeLivePlayerDef.VeLivePlayerResolution.VeLivePlayerResolutionOrigin); + playStream.streamType = VeLivePlayerDef.VeLivePlayerStreamType.VeLivePlayerStreamTypeMain; // 创建 VeLivePlayerStreamData VeLivePlayerStreamData streamData = new VeLivePlayerStreamData(); streamData.mainStreamList = new ArrayList<>(); -// 添加 RTM 流地址 - streamData.mainStreamList.add(playStreamRTM); + // 添加流 + streamData.mainStreamList.add(playStream); // 配置默认 format 和 protocol - streamData.defaultFormat = VeLivePlayerDef.VeLivePlayerFormat.VeLivePlayerFormatFLV; - streamData.defaultProtocol = VeLivePlayerDef.VeLivePlayerProtocol.VeLivePlayerProtocolTCP; + streamData.defaultFormat = streamFormat; + streamData.defaultProtocol = streamProtocol; -// 配置播放源 + // 配置播放源 livePlayer.setPlayStreamData(streamData); -// 开始播放 + // 开始播放 livePlayer.play(); - -// livePlayer.setPlayUrl(url); -// livePlayer.play(); }); btnStopPull.setOnClickListener(v -> livePlayer.stop()); } diff --git a/Android/APIExample/app/src/main/res/layout/activity_pull_stream.xml b/Android/APIExample/app/src/main/res/layout/activity_pull_stream.xml index 413179cf..d7fe0d6a 100644 --- a/Android/APIExample/app/src/main/res/layout/activity_pull_stream.xml +++ b/Android/APIExample/app/src/main/res/layout/activity_pull_stream.xml @@ -12,6 +12,36 @@ android:layout_width="match_parent" android:layout_height="400dp" /> + + + + + + + + + + + - - - - - - - \ No newline at end of file diff --git a/Android/APIExample/settings.gradle b/Android/APIExample/settings.gradle index c7c9c060..3be91350 100644 --- a/Android/APIExample/settings.gradle +++ b/Android/APIExample/settings.gradle @@ -14,6 +14,16 @@ dependencyResolutionManagement { mavenCentral() maven { url 'https://artifact.bytedance.com/repository/Volcengine/' } maven { url 'https://maven.faceunity.com/repository/maven-public/' } + maven { + url "https://artifact.bytedance.com/repository/thrall_base/" + credentials { + username = 'veVOS' + password = 'KUC9TpKrqbryrxHz' + } + authentication { + digest(BasicAuthentication) + } + } } } rootProject.name = "APIExample" diff --git a/iOS/ApiExample/ApiExample/ImportantComponents/PullRTMP/PullRTMPViewController.swift b/iOS/ApiExample/ApiExample/ImportantComponents/PullRTMP/PullRTMPViewController.swift index 6ddf8342..2bceec1b 100644 --- a/iOS/ApiExample/ApiExample/ImportantComponents/PullRTMP/PullRTMPViewController.swift +++ b/iOS/ApiExample/ApiExample/ImportantComponents/PullRTMP/PullRTMPViewController.swift @@ -8,9 +8,12 @@ import Foundation import TTSDKFramework -class PullRTMPViewController: BaseViewController,VeLivePlayerObserver { +class PullRTMPViewController: BaseViewController, VeLivePlayerObserver { var livePlayer: TVLManager? + // TVL拉流 或 RTM拉流,默认TVL拉流 + var streamFormat: VeLivePlayerFormat = .FLV + var streamProtocol: VeLivePlayerProtocol = .TCP override func viewDidLoad() { super.viewDidLoad() @@ -47,6 +50,8 @@ class PullRTMPViewController: BaseViewController,VeLivePlayerObserver { } func buildTVEngine() -> Void { + print(TTSDKManager.sdkVersionString) + // 创建播放器 self.livePlayer = TVLManager.init() @@ -63,7 +68,7 @@ class PullRTMPViewController: BaseViewController,VeLivePlayerObserver { print("setConfig") self.livePlayer?.setConfig(config) - self.livePlayer?.setObserver(self) + self.livePlayer?.setObserver(self) } func buildRenderView() -> Void { @@ -87,20 +92,41 @@ class PullRTMPViewController: BaseViewController,VeLivePlayerObserver { func createUI() -> Void { + view.addSubview(ttsdkVersionLabel) + view.addSubview(liveView) - view.addSubview(urlTextFieldView) + view.addSubview(streamTypeControl) + + view.addSubview(urlTextFieldView) view.addSubview(startButton) view.addSubview(stopButton) + view.addSubview(receivedSEILabel) + view.addSubview(receivedSEITextField) + liveView.snp.makeConstraints { make in make.top.equalTo(topView.snp.bottom) make.left.right.equalToSuperview() make.height.equalTo(liveView.snp.width) } + ttsdkVersionLabel.snp.makeConstraints { make in + make.top.equalTo(liveView.snp.bottom).offset(5) + make.left.equalToSuperview().offset(10) + make.right.equalToSuperview().offset(-10) + make.height.equalTo(30) + } + + streamTypeControl.snp.makeConstraints { make in + make.top.equalTo(ttsdkVersionLabel.snp.bottom).offset(10) + make.left.equalToSuperview().offset(10) + make.right.equalToSuperview().offset(-10) + make.height.equalTo(30) + } + urlTextFieldView.snp.makeConstraints { make in - make.top.equalTo(liveView.snp.bottom).offset(20) + make.top.equalTo(streamTypeControl.snp.bottom).offset(20) make.left.equalToSuperview().offset(10) make.right.equalToSuperview().offset(-10) make.height.equalTo(36) @@ -114,36 +140,63 @@ class PullRTMPViewController: BaseViewController,VeLivePlayerObserver { stopButton.snp.makeConstraints { make in make.centerY.equalTo(startButton) - make.left.equalTo(startButton.snp.right).offset(20) + make.left.equalTo(startButton.snp.right).offset(10) make.right.equalToSuperview().offset(-10) make.width.height.equalTo(startButton) } - view.addSubview(receivedSEILabel) - view.addSubview(receivedSEITextField) - receivedSEILabel.snp.makeConstraints { make in - make.top.equalTo(startButton.snp.bottom).offset(20) + make.top.equalTo(startButton.snp.bottom).offset(10) make.left.equalToSuperview().offset(10) make.right.equalToSuperview().offset(-10) make.height.equalTo(30) } receivedSEITextField.snp.makeConstraints { make in - make.top.equalTo(receivedSEILabel.snp.bottom).offset(20) + make.top.equalTo(receivedSEILabel.snp.bottom).offset(5) make.left.equalToSuperview().offset(10) make.right.equalToSuperview().offset(-10) - make.bottom.equalToSuperview().offset(-20) - + make.bottom.equalToSuperview().offset(-10) + make.height.equalTo(30) + } + } + + @objc func streamTypeControlChanged(_ sender: UISegmentedControl) { + let selectedStreamTypeIndex = sender.selectedSegmentIndex + + if selectedStreamTypeIndex == 0 { + streamFormat = .FLV + streamProtocol = .TCP + print("Selected stream type: FLV") + } else if selectedStreamTypeIndex == 1 { + streamFormat = .RTM + streamProtocol = .TLS + print("Selected stream type: RTM") } } @objc func startPull() { - if let text = self.urlTextFieldView.text, !text.isEmpty { - self.livePlayer?.setPlayUrl(text) + if let urlText = self.urlTextFieldView.text, !urlText.isEmpty { + ToastComponents.shared.show(withMessage: "当前拉流URL : \(urlText)") + + let playerStream = VeLivePlayerStream() + playerStream.url = urlText + playerStream.format = streamFormat + playerStream.resolution = .origin + playerStream.type = .main + + let streamData = VeLivePlayerStreamData() + streamData.mainStream = [playerStream] + streamData.defaultFormat = streamFormat + streamData.defaultProtocol = streamProtocol + + // 不再使用 self.livePlayer?.setPlayUrl(text) + self.livePlayer?.setPlay(streamData) self.livePlayer?.play() + + print("URL: \(urlText)") } else { - ToastComponents.shared.show(withMessage: "无效的推流地址") + ToastComponents.shared.show(withMessage: "拉流URL为空!") } } @@ -152,12 +205,26 @@ class PullRTMPViewController: BaseViewController,VeLivePlayerObserver { } // MARK: Lazy laod + lazy var ttsdkVersionLabel: UILabel = { + let label = UILabel() + label.text = "TTSDK版本:\(TTSDKManager.sdkVersionString)" + return label + }() + lazy var liveView: UIView = { let view = UIView.init() view.backgroundColor = .groupTableViewBackground return view }() + lazy var streamTypeControl: UISegmentedControl = { + let control = UISegmentedControl(items: ["FLV拉流", "RTM拉流"]) + control.selectedSegmentIndex = 0 // 默认FLV拉流 + control.addTarget(self, action: #selector(streamTypeControlChanged(_:)), for: .valueChanged) + control.translatesAutoresizingMaskIntoConstraints = false + return control + }() + lazy var urlTextFieldView: TextFieldView = { let settingView = TextFieldView() settingView.title = "拉流地址" @@ -196,7 +263,10 @@ class PullRTMPViewController: BaseViewController,VeLivePlayerObserver { // MARK: VeLivePlayerObserver func onError(_ player: TVLManager, error: VeLivePlayerError) { ToastComponents.shared.show(withMessage: "onError:\(error.errorMsg ?? "")") - + } + + func onFirstVideoFrameRender(_ player: TVLManager, isFirstFrame: Bool) { + ToastComponents.shared.show(withMessage: "First Video Frame Received") } func onReceiveSeiMessage(_ player: TVLManager, message: String) { @@ -206,8 +276,6 @@ class PullRTMPViewController: BaseViewController,VeLivePlayerObserver { func onPlayerStatusUpdate(_ player: TVLManager, status: VeLivePlayerStatus) { ToastComponents.shared.show(withMessage: "onPlayerStatusUpdate:\(status.rawValue)") - - } } diff --git a/iOS/ApiExample/Podfile b/iOS/ApiExample/Podfile index 14356512..3875248c 100644 --- a/iOS/ApiExample/Podfile +++ b/iOS/ApiExample/Podfile @@ -20,7 +20,7 @@ target 'ApiExample' do # 相芯美颜SDK pod 'FURenderKit', '8.7.0' - ## TT rtmp拉流sdk - pod 'TTSDKFramework', '1.40.3.6-premium', :subspecs => ['LivePull'] + # TTSDK RTMP拉流模块 + pod 'TTSDKFramework', '1.47.1.12-premium', :subspecs => ['LivePull-RTS'] end