diff --git a/docs/SellySDK_直播推拉流接入文档_Android.md b/docs/SellySDK_直播推拉流接入文档_Android.md index 842a804..9bfe549 100644 --- a/docs/SellySDK_直播推拉流接入文档_Android.md +++ b/docs/SellySDK_直播推拉流接入文档_Android.md @@ -18,6 +18,7 @@ Selly Live SDK 提供完整的音视频直播能力,支持 **推流(直播 - 拉流播放状态与错误回调 - 支持视频帧处理(美颜 / 滤镜 / 水印) - 基于 **Token 的安全鉴权机制** +- 支持 **RTMP H264 + AAC payload XOR 保护(可选)** --- @@ -103,6 +104,31 @@ dependencies { 2. 调用 `pusher.token = newToken` / `player.token = newToken` 3. 停止并重新开始推流 / 拉流流程 +### 4.4 RTMP Payload XOR 保护(可选) + +用途: + +- 防止他人拿到 RTMP 地址后直接播放、转码或截图 + +生效范围与约束: + +- 仅对 **RTMP** 生效 +- 仅支持 **H264 + AAC**(当前版本) +- 只处理 payload,配置帧(SPS/PPS、AAC Sequence Header)保持不变 +- 推流端与播放端必须使用**同一个 key** + +Key 格式: + +- `hex` 字符串,建议 16 或 32 字节(即 32/64 个 hex 字符) +- 支持 `0x` 前缀 +- 长度必须为偶数 +- 非法 key 会被忽略并关闭 XOR(会输出 warning 日志) + +时机要求: + +- 推流:请在 `startLiveWithStreamId(...)` / `startLiveWithUrl(...)` 之前设置 key +- 拉流:请在 `initWithStreamId(...)` / `initWithUrl(...)` 创建播放器时传入 `xorKeyHex` + --- ## 5. 推流接入详解 @@ -146,6 +172,17 @@ pusher.startRunning( pusher.token = pushToken ``` +#### RTMP Payload XOR(可选) + +```kotlin +val xorKeyHex = "A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6" + +// 建议在 startLiveWith... 之前设置 +pusher.setXorKey(xorKeyHex) +``` + +> 若在推流中修改 key,需停止并重新开始推流后才会使用新 key。 + ### 5.4 开始/停止推流 ```kotlin @@ -228,6 +265,7 @@ pusher.stopLive { error -> - `startRunning(cameraPosition, videoConfig, audioConfig)`:开始采集预览 - `setVideoConfiguration(config)`:更新视频参数 +- `setXorKey(hexKey)`:设置 RTMP payload XOR key(可选) - `startLiveWithStreamId(streamId)`:使用 streamId 推流 - `startLiveWithUrl(url)`:使用完整 URL 推流 - `stopLive()` / `stopLive(callback)`:停止推流 @@ -263,10 +301,11 @@ pusher.stopLive { error -> val player = SellyLiveVideoPlayer.initWithStreamId( context = this, streamId = streamId, - liveMode = SellyLiveMode.RTC + liveMode = SellyLiveMode.RTC, + xorKeyHex = "" // RTC 场景可留空 ) // 或直接使用完整 URL -// val player = SellyLiveVideoPlayer.initWithUrl(this, playUrl) +// val player = SellyLiveVideoPlayer.initWithUrl(this, playUrl, xorKeyHex = "A1B2...") ``` 若需要指定 `vhost` / `appName`: @@ -277,10 +316,13 @@ val player = SellyLiveVideoPlayer.initWithStreamId( streamId = streamId, liveMode = SellyLiveMode.RTMP, vhost = "your-vhost", - appName = "live" + appName = "live", + xorKeyHex = "A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6" ) ``` +> 使用 RTMP 加密流时,请在创建播放器时传入 `xorKeyHex`;后续如需换 key,请重建播放器实例。 + ### 6.2 设置拉流 Token(使用 streamId 时) ```kotlin @@ -336,8 +378,8 @@ player.delegate = object : SellyLiveVideoPlayerDelegate { 创建与渲染: -- `initWithStreamId(context, streamId, liveMode, vhost, appName)`:使用 streamId 创建播放器 -- `initWithUrl(context, url)`:使用完整 URL 创建播放器 +- `initWithStreamId(context, streamId, liveMode, vhost, appName, xorKeyHex)`:使用 streamId 创建播放器 +- `initWithUrl(context, url, xorKeyHex)`:使用完整 URL 创建播放器 - `attachRenderView(container)` / `setRenderView(view)`:设置渲染 View - `getRenderView()`:获取当前渲染 View @@ -388,7 +430,7 @@ player.delegate = object : SellyLiveVideoPlayerDelegate { SDK 不解析 URL 中的鉴权信息,所有鉴权均通过 `token` 属性完成。 ### Q2:运行中修改 Token 是否生效? -**A:** +**A:** 运行中修改 Token **不会影响当前已建立的连接**。 **下次重连或重新启动推流 / 拉流时会使用新的 Token**。 @@ -399,3 +441,11 @@ SDK 不解析 URL 中的鉴权信息,所有鉴权均通过 `token` 属性完 - 确认当前网络连接正常 - 查看播放器回调中的错误信息 - 确认视频流格式是否被 SDK 支持 + +### Q4:加密流播放花屏/噪音怎么办? +**A:** 重点检查以下项: + +- 推流端与播放端 `xorKeyHex` 是否完全一致 +- key 格式是否为合法 hex(偶数长度,支持 `0x` 前缀) +- 当前是否为 RTMP + H264 + AAC +- 变更 key 后是否已重启推流 / 重建播放器 diff --git a/example/libs/sellycloudsdk-1.0.0.aar b/example/libs/sellycloudsdk-1.0.0.aar index e59039d..1414a03 100644 Binary files a/example/libs/sellycloudsdk-1.0.0.aar and b/example/libs/sellycloudsdk-1.0.0.aar differ diff --git a/example/src/main/java/com/demo/SellyCloudSDK/FeatureHubActivity.kt b/example/src/main/java/com/demo/SellyCloudSDK/FeatureHubActivity.kt index b1ae06e..093e38d 100644 --- a/example/src/main/java/com/demo/SellyCloudSDK/FeatureHubActivity.kt +++ b/example/src/main/java/com/demo/SellyCloudSDK/FeatureHubActivity.kt @@ -344,6 +344,7 @@ class FeatureHubActivity : AppCompatActivity() { dialogBinding.etFps.setText(current.fps.toString()) dialogBinding.etMaxBitrate.setText(current.maxBitrateKbps.toString()) dialogBinding.etMinBitrate.setText(current.minBitrateKbps.toString()) + dialogBinding.etXorKey.setText(current.xorKeyHex) dialogBinding.rgResolution.check( when (current.resolution) { AvDemoSettings.Resolution.P360 -> R.id.rbRes360p @@ -384,12 +385,14 @@ class FeatureHubActivity : AppCompatActivity() { else -> AvDemoSettings.Resolution.P720 } + val xorKey = dialogBinding.etXorKey.text?.toString()?.trim().orEmpty() val updated = current.copy( streamId = streamId, resolution = res, fps = fps, maxBitrateKbps = maxKbps, - minBitrateKbps = minKbps + minBitrateKbps = minKbps, + xorKeyHex = xorKey ) settingsStore.write(updated) dialog.dismiss() @@ -413,13 +416,14 @@ class FeatureHubActivity : AppCompatActivity() { Toast.makeText(this, "请输入 Stream ID 或 URL", Toast.LENGTH_SHORT).show() return@setOnClickListener } + val xorKey = dialogBinding.etXorKey.text?.toString()?.trim().orEmpty() val liveMode = if (dialogBinding.rbRtc.isChecked) { SellyLiveMode.RTC } else { SellyLiveMode.RTMP } dialog.dismiss() - startActivity(LivePlayActivity.createIntent(this, liveMode, input, autoStart = true)) + startActivity(LivePlayActivity.createIntent(this, liveMode, input, autoStart = true, xorKeyHex = xorKey)) } dialog.show() diff --git a/example/src/main/java/com/demo/SellyCloudSDK/avdemo/AvDemoSettingsStore.kt b/example/src/main/java/com/demo/SellyCloudSDK/avdemo/AvDemoSettingsStore.kt index 8547bf5..c80e86e 100644 --- a/example/src/main/java/com/demo/SellyCloudSDK/avdemo/AvDemoSettingsStore.kt +++ b/example/src/main/java/com/demo/SellyCloudSDK/avdemo/AvDemoSettingsStore.kt @@ -9,6 +9,7 @@ data class AvDemoSettings( val fps: Int, val maxBitrateKbps: Int, val minBitrateKbps: Int, + val xorKeyHex: String = "", ) { enum class Resolution { P360, P480, P540, P720 } @@ -36,7 +37,8 @@ class AvDemoSettingsStore(context: Context) { resolution = resolution, fps = prefs.getInt(KEY_FPS, DEFAULT_FPS), maxBitrateKbps = prefs.getInt(KEY_MAX_KBPS, DEFAULT_MAX_KBPS), - minBitrateKbps = prefs.getInt(KEY_MIN_KBPS, DEFAULT_MIN_KBPS) + minBitrateKbps = prefs.getInt(KEY_MIN_KBPS, DEFAULT_MIN_KBPS), + xorKeyHex = prefs.getString(KEY_XOR_KEY_HEX, "").orEmpty() ) } @@ -47,6 +49,7 @@ class AvDemoSettingsStore(context: Context) { putInt(KEY_FPS, settings.fps) putInt(KEY_MAX_KBPS, settings.maxBitrateKbps) putInt(KEY_MIN_KBPS, settings.minBitrateKbps) + putString(KEY_XOR_KEY_HEX, settings.xorKeyHex) } } @@ -62,5 +65,6 @@ class AvDemoSettingsStore(context: Context) { private const val DEFAULT_FPS = 30 private const val DEFAULT_MAX_KBPS = 2000 private const val DEFAULT_MIN_KBPS = 500 + private const val KEY_XOR_KEY_HEX = "xor_key_hex" } } diff --git a/example/src/main/java/com/demo/SellyCloudSDK/live/LivePlayActivity.kt b/example/src/main/java/com/demo/SellyCloudSDK/live/LivePlayActivity.kt index 277137f..d9beb72 100644 --- a/example/src/main/java/com/demo/SellyCloudSDK/live/LivePlayActivity.kt +++ b/example/src/main/java/com/demo/SellyCloudSDK/live/LivePlayActivity.kt @@ -779,17 +779,20 @@ class LivePlayActivity : AppCompatActivity() { const val EXTRA_PLAY_STREAM_NAME = "play_stream_name" const val EXTRA_AUTO_START = "auto_start" const val EXTRA_PREVIEW_IMAGE_URL = "preview_image_url" + const val EXTRA_XOR_KEY_HEX = "xor_key_hex" fun createIntent( context: Context, liveMode: SellyLiveMode, streamIdOrUrl: String, - autoStart: Boolean = true + autoStart: Boolean = true, + xorKeyHex: String = "" ): Intent { return Intent(context, LivePlayActivity::class.java) .putExtra(EXTRA_PLAY_PROTOCOL, liveMode.name) .putExtra(EXTRA_STREAM_ID_OR_URL, streamIdOrUrl) .putExtra(EXTRA_AUTO_START, autoStart) + .putExtra(EXTRA_XOR_KEY_HEX, xorKeyHex) } fun createIntentWithParams( @@ -844,7 +847,8 @@ class LivePlayActivity : AppCompatActivity() { val streamIdOrUrl: String, val autoStart: Boolean, val playParams: PlayParams?, - val previewImageUrl: String? + val previewImageUrl: String?, + val xorKeyHex: String = "" ) { companion object { fun from(intent: Intent, env: LiveEnvSettings): Args { @@ -865,13 +869,15 @@ class LivePlayActivity : AppCompatActivity() { val input = intent.getStringExtra(EXTRA_STREAM_ID_OR_URL).orEmpty() .ifBlank { playParams?.streamName ?: env.defaultStreamId } val autoStart = intent.getBooleanExtra(EXTRA_AUTO_START, true) + val xorKeyHex = intent.getStringExtra(EXTRA_XOR_KEY_HEX).orEmpty().trim() val mode = resolveLiveMode(rawProtocol, input, env) return Args( liveMode = mode, streamIdOrUrl = input, autoStart = autoStart, playParams = playParams, - previewImageUrl = previewImageUrl + previewImageUrl = previewImageUrl, + xorKeyHex = xorKeyHex ) } @@ -908,11 +914,12 @@ class LivePlayActivity : AppCompatActivity() { args.playParams.streamName, liveMode = args.liveMode, vhost = args.playParams.vhost, - appName = args.playParams.appName + appName = args.playParams.appName, + xorKeyHex = args.xorKeyHex ) } - input.contains("://") -> SellyLiveVideoPlayer.initWithUrl(this, input) - else -> SellyLiveVideoPlayer.initWithStreamId(this, input, liveMode = args.liveMode) + input.contains("://") -> SellyLiveVideoPlayer.initWithUrl(this, input, xorKeyHex = args.xorKeyHex) + else -> SellyLiveVideoPlayer.initWithStreamId(this, input, liveMode = args.liveMode, xorKeyHex = args.xorKeyHex) } } } diff --git a/example/src/main/java/com/demo/SellyCloudSDK/live/LivePushActivity.kt b/example/src/main/java/com/demo/SellyCloudSDK/live/LivePushActivity.kt index 947eb82..140fe8c 100644 --- a/example/src/main/java/com/demo/SellyCloudSDK/live/LivePushActivity.kt +++ b/example/src/main/java/com/demo/SellyCloudSDK/live/LivePushActivity.kt @@ -144,6 +144,7 @@ class LivePushActivity : AppCompatActivity() { ) applyStreamConfig(settings) pusher.token = auth?.tokenResult?.token + pusher.setXorKey(settings.xorKeyHex) pusher.startLiveWithStreamId(streamId) } } @@ -743,6 +744,7 @@ class LivePushActivity : AppCompatActivity() { dialogBinding.etFps.setText(current.fps.toString()) dialogBinding.etMaxBitrate.setText(current.maxBitrateKbps.toString()) dialogBinding.etMinBitrate.setText(current.minBitrateKbps.toString()) + dialogBinding.etXorKey.setText(current.xorKeyHex) dialogBinding.etEnvVhost.setText(currentEnv.vhost) dialogBinding.etEnvVhostKey.setText(currentEnv.vhostKey) dialogBinding.etEnvAppId.setText(currentEnv.appId) @@ -789,12 +791,14 @@ class LivePushActivity : AppCompatActivity() { else -> AvDemoSettings.Resolution.P720 } + val xorKey = dialogBinding.etXorKey.text?.toString()?.trim().orEmpty() val updated = current.copy( streamId = streamId, resolution = res, fps = fps, maxBitrateKbps = maxKbps, - minBitrateKbps = minKbps + minBitrateKbps = minKbps, + xorKeyHex = xorKey ) settingsStore.write(updated) val envUpdated = currentEnv.copy( diff --git a/example/src/main/res/layout/dialog_live_preset_settings.xml b/example/src/main/res/layout/dialog_live_preset_settings.xml index 2807dfe..fa75370 100644 --- a/example/src/main/res/layout/dialog_live_preset_settings.xml +++ b/example/src/main/res/layout/dialog_live_preset_settings.xml @@ -198,6 +198,30 @@ android:textColorHint="@color/av_text_hint" android:textSize="14sp" /> + + + +