Android SDK 与原生 Media3
下载 Android 播放器 使用 video 控件, 可拖动播放列表, 保存 shuffle/ repeat 和 离线 恢复 。 使用 DRM-X 或 本地 Media3 整合受保护的播放 。
试试看 Android 播放器#
1.0.0-preview.12 · 7.02 MB · Android 6.0+ (API 23+) · DRM-X 6.0 Player · 更新 2026-09-14
- 在您的Android 手机上打开此页面并下载APK 。
- 打开已下载的文件。 如果 Android 询问, 允许从您使用的浏览器或文件管理器安装, 然后点击 Install 。
- 打开 DRM-X 6.0 播放器( T),保持连接,并窃听视频的 播放( L) 按钮。当您按下 Play 键时,应用程序请求获得流媒体许可证。
- 打开 设置 以选择质量、 音频、 标题或回放速度 。 旋转电话继续全屏 。
- 磁带 下载 用于当前视频或 下载播放列表 向所有人开放 VOD 下载时使用最多可达1080p的最高许可引渡。 L1,或 480p 打开 L3. 下线播放 打开已完成的本地播放列表,其中包含下一张/先前的、洗牌、重复和自动播放; 上线 返回完整播放列表。 更新 连接时更新所选许可证 。
已经排队的媒体下载在背景中继续。 如果Android 在准备许可证或音轨时停止应用程序, 请在连接时重新打开它以完成剩余项目的准备。 现有的下载被保留。 离线播放只使用缓存媒体; 不完整或过期的下载需要完成或更新 。
Widevine 供应失败(Media3 6002现在使用认证的演示服务器中继器到官方的Google服务。 L3 CDM 能够接收 SD 键。没有工作的设备 Widevine 执行需要设备/软件固定;许可证不能添加缺失 DRM 支助。
APK 样本是公开的, 并签名进行评价 。 下面的AAR 和完整的SDK 仍然通过符合条件的控制台账户提供 。 请将您自己的生产应用程序签名密钥和后端登录与此演示分开 。
APK校验和USB安装
SHA-256:
8fa044b82c0abab7ce2a987f349524fd7ad1e78f3c897771f4764cb2aa521180使用Android平台工具及授权的USB设备:
adb install -r drmx-android-player-1.0.0-preview.12.apk安装此 APK 重置现有的正式样本, 而不进行复选。 同样的签名密钥和应用程序 ID 保存下载、 离线许可、 保存位置和首选项。 如果您的构建使用了另一个签名密钥, 请使用单独的应用程序 。 ID 并保存样本的离线许可
有什么新鲜的 Android 播放器 预览. 12#
2026年9月14日发布. Fixs下载的回放在一次意外的许可请求中, Widevine 许可证过期或剩余1分钟或更短时间。这可能会显示为 Media3 错误 6004,一个 HTTP 连接时出错,或 a DNS 断开时出错 。
播放器 现在在本地恢复下载的许可证。 有效的许可证保留了剩余播放时间; 过期的许可证显示 连接和磁盘更新 。 许可证的有效性和保护规则仍然由设备执行。 诊断还包括当网络操作失败时的数字HTTP 状态, 不披露请求头、 URL 或许可证回复 。
- 在现有的官方应用程序上安装预览 12。 保留您的当前应用程序数据和下载 。
- 如果WayMaker或另一部下载的视频需要更新,连接到互联网上,选择该视频并点击再续.
- 等待 License 更新。 准备下线播放 , 然后选择 Play 下线 。 加密视频仍然下载 。
本版保留了预览11的控件和播放列表功能. 公共评价app及其嵌入式播放器识别预览12; 单独发行的ConsoleSDK仍然保留预览10/捆绑20260910.8.
有什么新鲜的 Android 播放器 预览 11#
2026年9月14日发布. 本评价app包括最新的播放器控制和播放列表改进,在我们Android和Apple播放器之间具有一致的交互风格.
- 视频内部的控制 : 在多视频播放列表、CC 设置和全屏按钮上以 Play/Pause、 President 和 Next 居中。 播放时, 控制会隐藏两秒后; 点击视频再次显示它们 。
- 说明和重播设置 : 使用情况 CC 显示或隐藏可用的字幕,设置则选择标题语言、音频、播放速度和允许的质量。离线选择只使用下载中包含的音轨。
- 拖曳重排 : 拖曳播放列表项的控件以改变其位置。 保存的命令在应用程序重新启动后继续运行, 并反映在下载的播放列表中。 重排会保持当前视频的播放。 Tap the handle for accessable Move up/ moving actions 。
- 保存的洗牌并重复 : 选择的模式在网络和离线播放时会保持突出显示和幸存的重新启动。 使用洗牌和重复两种方式, 视频会遵循保存的顺序, 在最后的项目后停止。 播放列表菜单的“ 播放下一个” 动作已被删除 。
- 恢复您所停止的位置 : 每段视频保存的VOD 位置在您返回时恢复 。 此设备为当前查看器保存了进度; 它不会在设备之间同步 。
播放器的更改在三星S24 Ultra上被验证,包括播放列表在播放时拖曳,重启后保存了命令,播放恢复和下载了Wi-Fi的播放,移动数据被禁用. 离线查看仍然需要完成下载和可用的,未过期的离线许可证.
本版更新了公共评价APK. 单独发行的ConsoleSDK仍为1.0.0-preview.10,back20260910.8;其下面现有的集成指令指的是该捆.
1. 选择集成路径#
使用 DrmXPlayer、 电话样本和离线管理器。 请保留自己的登录和业务规则 。
PATH B. 妇女 KEEP YOUR UIMedia3 + DRM-X 帮助者保留已有的 播放器 并下载堆栈。 让 DrmXOfflineManager 管理持续许可证 。
路径 C · 仅原生集成Media3 没有AAR自己执行相同的后端合同和本地许可证的生命周期。 不 DRM-X播放器 依赖 。
在控制台 → 集成中下载 Android SDK ZIP 或 AAR。订阅有效的专业版($499/月)和企业版组织可以下载。试用账户和标准版账户不能下载 SDK。本文档及其中的原生 Media3 示例公开提供;实际播放仍需要具有访问资格的账户、已发布内容和兼容设备。
当前预览版本:1.0.0-preview.10 · 客户 SDK 包:20260910.8 · Media3 1.11.0。ZIP 包含本地 Maven 仓库、Gradle wrapper、带注释的 README、手机/电视/原生集成示例、位于 apks/ 的可安装手机示例 APK,以及离线集成说明。包内不含客户凭据或受保护的视频。
2. 准备内容和后端#
- 加密输出包和发布 a Widevine- 兼容加密 DASH 标题 DRM-X 环境,复制其准确性 已出版 Content ID.
- 配置您的服务器 Environment ID, Site Key 和 Access Key使用时 可下载文件 PHP 融合 一个工作起点。
- 执行您的用户登录和权限检查。 您的应用程序以短命的用户代号调用您的后端; 您的后端选择内容、 政策和可信任的 URL 。
- 首先在物理 Widevine 能力设备上运行在线播放。 仅在流线合同工程完成后才添加离线获取 。
| 更改此值 | 属于何处 | 含义 |
|---|---|---|
| 会话结束点 | Android | 您的 HTTPS 播放会话结束点, 而不是原始许可服务器 URL 。 |
| 离线端点 | Android,只下线 | 您的获取/ 更新/ 释放授权结束点 。 |
| 内容编号 | Android 请求; 后端验证 | 已发布保护的Content ID观看者有权访问. |
| 客户访问令牌 | 登录后应用内存 | 您的短命用户代号, 绝不是Site Key 或Access Key 。 |
| Environment ID、Site Key、Access Key | 只有服务器密钥配置 | DRM-X 账户 凭据永远不要把它们装进... APK, HTML 或源控。 |
| 政策和可信赖的列表 URL | 您的后端 | 商业权益和加密输出包映射。 请不要接受应用程序中的这些 。 |
3. 安装和运行 SDK#
在 Android Studio 中打开解压后的 ZIP 根文件夹。使用 JDK 17、Android SDK 36 和 Android Gradle Plugin 9.2.0;随附的 wrapper 使用 Gradle 9.4.1。最低设备 API 级别为 23,下载构建依赖需要联网。选择 samples.phone 并运行。初始配置使用公共演示后端和示例内容;先按照登录和在线播放流程测试,再切换到自己的后端。
# Windows
gradlew.bat :samples:phone:assembleDebug
# macOS / Linux
sh gradlew :samples:phone:assembleDebug
对于已有的应用程序, 请将 ZIP 的 maven/ 文件夹复制到您的工程根 。 在 settings.gradle.kts 中 :
dependencyResolutionManagement {
repositories {
maven { url = uri(rootDir.resolve("maven")) }
google()
mavenCentral()
}
}
在您的应用程序模块 build.gradle.kts 中 :
dependencies {
implementation("com.haihaisoft.drmx:drmx-universal-player:1.0.0-preview.10")
}
正在安装独立的 AAR 而不是
放下枪 AAR 在您的应用模块中 libs/。一个公寓 AAR 没有自动的过渡依赖解析,所以添加三个匹配 Media3 依赖性 。 不要安装两个窗体 。
implementation(files("libs/drmx-universal-player-1.0.0-preview.10.aar"))
implementation("androidx.media3:media3-exoplayer:1.11.0")
implementation("androidx.media3:media3-exoplayer-dash:1.11.0")
implementation("androidx.media3:media3-ui:1.11.0")在AndroidManifest.xml 中, 允许此最小样本的互联网访问并禁用备份 。 在需要备份的制作应用程序中, 具体排除设备绑定的离线许可元数据 。
<uses-permission android:name="android.permission.INTERNET" />
<application android:allowBackup="false" android:usesCleartextTraffic="false" ... >
搜索 drmx.sessionEndpoint 、 drmx.offlineEndpoint 、 drmx.contentId 和 customer.loginEndpoint 的手机样本 MainActivity.kt 。 更改标记的倒置设置, 或在登录后从您的应用程序中通过这些输入的额外设置 。 在运行时间通过customer.accessToken 。 ZIPREADME 解释每个参数, 以及要替换的演示钩 。
使用 AAR# 构建测试APK
此示例使用全程可运行的电话项目 SDK ZIP 并明确链接独立 AAR。同一方法对现有的 Android 应用。
- Download and extract the full Android SDK ZIP. Open its root folder in Android Studio.
- Keep
libs/drmx-universal-player-1.0.0-preview.10.aarin the extracted root. Use JDK 17, Android SDK 36 and the included Gradle wrapper. - 在
samples/phone/build.gradle.kts中, 将 DRM-X Maven 依赖性 替换为dependencies中的以下条目。 保留其余的构建文件 。
implementation(files(rootProject.file("libs/drmx-universal-player-1.0.0-preview.10.aar")))
implementation("androidx.media3:media3-exoplayer:1.11.0")
implementation("androidx.media3:media3-exoplayer-dash:1.11.0")
implementation("androidx.media3:media3-ui:1.11.0")
删除 implementation("com.haihaisoft.drmx:drmx-universal-player:1.0.0-preview.10") 因此, 相同的 SDK 不包括两次 。 平面 AAR 需要上面显示的三个 Media3 依赖 。
- 要将下载的演示文稿与您自己的测试构建一起安装, 请将手机模块的
applicationId更改为com.example.drmxtest。 保留其名称空间不变 。 - 构建 APK,然后安装到一个 USB-已连接 Android 设备为 USB 已授权调试 。
# Windows, from the extracted SDK root
gradlew.bat :samples:phone:assembleDebug
adb install -r samples/phone/build/outputs/apk/debug/phone-debug.apk
# macOS / Linux
sh gradlew :samples:phone:assembleDebug
adb install -r samples/phone/build/outputs/apk/debug/phone-debug.apk
打开测试的应用和twap Play。 提供的电话源最初使用相同的公共演示。 要测试您自己的标题, 请在 MainActivity.kt 中替换标记的会话/ 关闭/ 登录端点并发布 Content ID , 并在运行时提供您用户的短寿命认证 。 您的服务器保存 Site Key 和Access Key 。
紧凑的控制来自 DrmXPlayerView 输入 AAR。提供的活动显示游戏对授权、响应旋转、下载所有权、离线播放和续作。见 控制集成实例 将此视图添加到您的应用程序中时, 请在下面 。
4. 服务器请求和默认政策#
那个... Android app 请求返回播放会话的后端。 您的后端认证了用户, 验证权限, 映射 Content ID 媒体和电话 DRM-X。见 播放会话 API 整个串流合同。
{
"contentId": "YOUR-PUBLISHED-CONTENT-ID",
"contentType": "vod",
"drmSystem": "widevine",
"playbackMode": "streaming",
"platformCapabilities": {
"clientPlatform": "android",
"drmSystems": ["widevine"], "manifestTypes": ["dash"],
"maximumHeight": 1080, "secureDecoder": false, "persistentState": false
}
}
上面的保守能力不能断言WidevineL1. 使用SDK 的能力助手, 或您自己的设备评估, 用于实际的设备。 客户端的能力帮助选择兼容性; 它们从未授予质量权限 。
PHP 样本默认为multi-tier-standard 用于在线播放 。 当您的加密输出包使用一个密钥时, 请选择它所记录的单键策略。 您的政策必须匹配加密输出包的密钥模型和查看者的权利 。 更改服务器配置或它的DRMX_LICENSE_POLICY_TEMPLATE 环境设置, 而不是播放器 中的查询参数 。 见 许可证政策 。
playback_policy.persistent 在服务器发布的政策和选择许可证、租赁、回放和续订期限中真实存在。参考文献分别使用7天/3天/48小时/7天;这些是实例商业规则,而不是工作室批准或全平台默认。返回匹配的元数据。许可证 Widevine 仍然具有权威性。5. 路径A:通过 DrmXPlayer 播放#
在 Activity 中创建 PlayerView,使用安全窗口,并在登录后调用 prepare。封装层负责交换会话、配置 Widevine、应用返回的最大视频高度限制,并在停止时释放流播放预留名额。所需类型来自 com.haihaisoft.drmx.player 和 AndroidX Media3。
val view = PlayerView(this)
setContentView(view)
DrmXPlayer.protectWindow(window)
val player = DrmXPlayer(this, view)
val capabilities = WidevineCapabilities.playbackCapabilities(
clientPlatform = "android",
displayMetrics = resources.displayMetrics,
applicationVersion = "1.0",
)
player.prepare(
endpoint = "https://customer.example/api/drmx/playback-session",
contentId = "YOUR-PUBLISHED-CONTENT-ID",
contentType = "vod", // use live only for a published live title
capabilities = capabilities,
customerAuthenticationHeaders = mapOf("Authorization" to "Bearer $userToken"),
)
// Keep player as an Activity/Fragment-owned field in your actual application.
// Stop when your playback lifecycle ends; close when the owner is destroyed.
// player.stop()
// player.close()
注册 DrmXPlayerListener 用于授权、 准备、 播放和错误 UI 。 显示安全错误代码, 而不是丢弃许可证牌或响应机构 。 使用包含的电话样本进行整个生命周期、 全屏控制和下载队列 。
播放器控制和不间断旋转#
Tap Play 在视频上开始授权。 打开屏幕或设置不会获得流媒体许可证。 暂停/ 恢复, 更改设置并旋转电话保存当前会话 。

| 控件 | 顾客能做的 |
|---|---|
| 质量 | 选择 Auto 或 支持的、 许可的解析度 。 无法获取的选项解释许可证或设备/ DRM 限制 。 Auto 遵守相同的限制 。 |
| 音频轨道 | 选择加密输出包提供的任何支持的音频语言 。 |
| 字幕/CC (简体中文). | 选择字幕语言或关闭。 当支持的字幕存在并直接切换时, 将会出现 CC 按钮 。 |
| 说明外观 | 调整字体、文本颜色和大小、背景/窗口颜色和不透明。在设备上保存首选项。 |
| 回放速度/ 睡眠计时器 | 在选择间隔后以0.25–2×速度自动播放或暂停. |
| 搜索/ 全屏 | 刷新时间线, 寻求 10 秒, 或者双拖曳任意一面。 旋转到地貌, 不重新开始播放 。 |
图片下的三个紧凑动作是 Download(供下线使用),Play offline和(续订(离线许可证)). 样本下载允许的视频引渡和所支持的音频/字幕音轨;离线菜单只曝光下载的媒体. 更新需要连通.
复制日志 在信息页脚复制最近错误为文本的同时, 您可以不带截图地发送细节 。 报告包含一个报告ID, 操作, SDK/Android/ device版本, CDM/Media3 错误, 选定的堆栈框, 连接/ 下载/ 回放状态和重复计数。 它们不包括凭据、 媒体 URL、 密钥、 密钥集 ID、 许可证数据和 CDM 全部的垃圾 。
样本保留了最多32份当地报告(64份) KB),5小时。在连接和签名时,它上传到已配置的客户端后端等待错误,最多每30秒一次。公共演示服务器保留警告/错误5小时,结合重复事件,将文件封顶为512 KB 每分钟一次。常规进程和成功回放没有被记录。自定义集成必须明确提供自己的来源。 drmx.diagnosticsEndpoint;诊断错误从不中断播放.
val player = DrmXPlayer(this)
val controls = DrmXPlayerView(this)
controls.bind(player) // Does not acquire a session or license.
controls.onPlayRequested = {
player.prepare(endpoint = sessionEndpoint, contentId = contentId,
capabilities = capabilities, customerAuthenticationHeaders = userHeaders)
}
controls.onFullScreenRequested = { /* resize the existing player view */ }
player.listener = object : DrmXPlayerListener {
override fun onAuthorizing(contentId: String) { controls.setPreparing(true) }
override fun onReady(info: DrmXPlaybackInfo) { controls.setPreparing(false) }
override fun onError(error: PlaybackSessionException) { controls.setPreparing(false) }
}
// Keep protectWindow(window) and the secure video surface.
// At final owner destruction: controls.unbind(); player.close()
提供的手机显示器处理方向和屏幕大小变化; 它的onConfigurationChanged 调整了相同的活动和播放器 大小。 在调整时保留两部分。 重现活动的主机应该保留它的回放所有者, 并重新输入视图。 处理死亡与旋转不同, 需要正常状态恢复 。 4K/8K 选项需要匹配内容、 许可证权限和设备/DRM 支持; CDM 仍然具有权威性 。
习惯 播放器,停止并释放活动离线 DRM 在检查、续签或发放同一持续许可证之前先进行。 SDK因为... ... player.stopForOfflineLicenseOperation { /* call manager here */ } 等待着 Media3 播放队列以释放媒体。 plain stop() 同步运行的队列。不单独执行 checkLicense() ; 播放本身验证其特性 CDM 状态。提供的样本显示完整的交接,并在更新时禁用重叠动作。
6. 获得许可证和下载媒体#
val offlineManager = DrmXOfflineManager(applicationContext)
val capabilities = WidevineCapabilities.offlinePlaybackCapabilities(
clientPlatform = "android", // android-tv or android-automotive where applicable
displayMetrics = resources.displayMetrics,
applicationVersion = "1.0",
)
offlineManager.acquireLicense(
endpoint = offlineEndpoint,
contentId = contentId,
capabilities = capabilities,
customerAuthenticationHeaders = customerHeaders,
) { result ->
result.onSuccess { stored ->
// Next: select tracks <= stored.maximumHeight and queue their download.
// Acquisition alone does not download the video.
}.onFailure { /* show a safe retry/error state */ }
}
那个... SDK 保存不透明 Widevine 密钥集 ID 和非机密元数据。 CDM 持有执照,不会持续 DRM License Tokens, 原始密钥、 挑战或许可响应字节。 排除 com.haihaisoft.drmx.offline.licenses.v1.xml 从备份。
创建一个应用程序拥有 Media3 SimpleCache、 下载管理器和数据库。使用 DownloadHelper 与获得许可的媒体项目和在内部选择视频 maximumHeight内。 Media3 1.11 准备的回调是: onPrepared(helper, tracksInfoUpdated)电话样本 enqueueDownload 和 OfflineTestRuntime 显示可运行的执行。
// Inside DownloadHelper's prepared callback, after selecting licensed video:
helper.addAudioLanguagesToSelection("en", "zh-Hans", "zh")
helper.addTextLanguagesToSelection(true, "en", "zh-Hans", "zh")
val selected = helper.getDownloadRequest(offlineLicenseId, byteArrayOf())
val request = offlineManager.createDownloadRequest(contentId, selected.streamKeys)
downloadManager.addDownload(request)
helper.release()
下载您打算下线的每一种语言。 调用 createDownloadRequest 不使用流键下载所有音轨, 这通常会浪费适应性视频。 显示下载进度并等待 Download.STATE_COMPLETED 。 对于制作背景传输, 请整合 Media3 DownloadService , 存储限制 和 Android 前景服务/ 通知 要求; 演示队列在进程内运行 。
7. 路径B:用现有的 Media3 播放器#
读取已完成的下载并使用其精确的流密钥。 下载和播放堆栈必须共享相同的缓存位置和密钥映射。 此示例会故意禁用上游访问, 严格地进行离线播放 :
val cacheOnly = CacheDataSource.Factory()
.setCache(downloadCache)
.setUpstreamDataSourceFactory(null)
val mediaSourceFactory = DefaultMediaSourceFactory(this)
.setDataSourceFactory(cacheOnly)
val nativePlayer = ExoPlayer.Builder(this)
.setMediaSourceFactory(mediaSourceFactory)
.build()
val completed = downloadIndex.getDownload(offlineLicenseId)
require(completed?.state == Download.STATE_COMPLETED)
val playback = offlineManager.createPlayback(contentId, streamKeys = completed!!.request.streamKeys)
nativePlayer.setMediaItem(playback.mediaItem)
nativePlayer.prepare()
nativePlayer.play()
// Release nativePlayer when the owner is destroyed.
要使用相同的与路径 A 的离线流, 用这个 mediaSourceFactory 构造 DrmXPlayer 并呼叫 player.prepareOffline(playback) 。 普通恢复使用本地的 CDM 许可证而不要求新的许可证 。 丢失的缓存条目会在断开接收时产生错误, 而不是静默地切换到互联网 。
8. 路径C:本土 Media3,没有 DRM-X AAR#
您不需要使用 DrmXPlayer 或 DrmXOfflineManager 。 添加步骤 3 的 Media3 依赖性, 并执行相同的客户后端合同 。 使用 session.manifestUrl (不是 session.manifest.url) 、 返回的许可证URL 和限定的授权头 。 在停止、 替换、 结束、 失败和销毁时应用返回的最大高度和处理流度释放 。
下面完整的本地活动被编译为samples:native 和 此SDK 发布 。 它处理HTTPS 、 边框响应读取、 背景授权、 stale 响应和保留发布 。 替换标记的终点/ Content ID , 并提供您自己的登录符号 。 示例是一个起点; 添加您的生产更新、 帐号切换和app- 导航生命周期 。
完全本地流线活动(Kotlin,不依赖DRM-X)
package com.haihaisoft.drmx.samples.nativeplayer
import android.app.Activity
import android.os.Bundle
import android.view.WindowManager
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URI
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
/** Complete native streaming example. No DRM-X AAR or server credentials. */
@UnstableApi
class MainActivity : Activity() {
private lateinit var player: ExoPlayer
private lateinit var status: TextView
private val worker = Executors.newSingleThreadExecutor()
private val closed = AtomicBoolean(false)
private var activeSession: JSONObject? = null
private var requesting = false
private var generation = 0
// CUSTOMIZE: use YOUR authenticated customer backend and Published Content ID.
// The sample can receive these after your application's login via Intent extras.
// Never replace the app login token with a DRM-X Site Key or Access Key.
private val endpoint get() = intent.getStringExtra("drmx.sessionEndpoint")
?: "https://customer.example/api/drmx/playback-session"
private val contentId get() = intent.getStringExtra("drmx.contentId") ?: "YOUR-PUBLISHED-CONTENT-ID"
private val customerToken get() = intent.getStringExtra("customer.accessToken").orEmpty()
override fun onCreate(state: Bundle?) {
super.onCreate(state)
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
val root = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL; fitsSystemWindows = true; setPadding(24, 24, 24, 24) }
status = TextView(this).apply { text = "Native Media3 · connect your backend, then Play"; textSize = 16f }
val view = PlayerView(this)
player = ExoPlayer.Builder(this).build(); view.player = player
root.addView(status)
root.addView(view, LinearLayout.LayoutParams(-1, 0, 1f))
root.addView(Button(this).apply { text = "Play protected video"; setOnClickListener { startPlayback() } })
root.addView(Button(this).apply { text = "Stop & release session"; setOnClickListener { stopPlayback() } })
setContentView(root)
player.addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) { status.text = "Playback error: ${error.errorCodeName}"; releaseSession() }
override fun onPlaybackStateChanged(state: Int) { if (state == Player.STATE_ENDED) releaseSession() }
})
}
private fun startPlayback() {
if (requesting) return
stopPlayback(); requesting = true; status.text = "Authorizing…"
val attempt = generation
val request = JSONObject().put("contentId", contentId).put("contentType", "vod")
.put("drmSystem", "widevine").put("playbackMode", "streaming")
.put("platformCapabilities", JSONObject().put("clientPlatform", "android")
.put("drmSystems", org.json.JSONArray(listOf("widevine")))
.put("manifestTypes", org.json.JSONArray(listOf("dash")))
.put("maximumHeight", 1080).put("secureDecoder", false).put("persistentState", false))
val headers = if (customerToken.isBlank()) emptyMap() else mapOf("Authorization" to "Bearer $customerToken")
worker.execute {
val result = runCatching { JSONObject(post(endpoint, request.toString(), headers)) }
runOnUiThread {
requesting = false
result.onSuccess { session ->
if (closed.get() || attempt != generation) { workerRelease(session); return@onSuccess }
// Keep release information even if contract validation fails.
activeSession = session
runCatching {
require(session.getInt("contractVersion") == 1 && session.getString("contentId") == contentId)
require(session.getString("drmSystem") == "widevine" && session.getString("playbackMode") == "streaming")
val drm = session.getJSONObject("drm")
require(drm.getString("system") == "widevine")
val maximumHeight = session.getJSONObject("policy").getInt("maximumHeight")
require(maximumHeight in 1..4320)
val mediaItem = MediaItem.Builder().setUri(https(session.getString("manifestUrl")))
.setMimeType("application/dash+xml")
.setDrmConfiguration(MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
.setLicenseUri(https(drm.getString("licenseServerUrl")))
.setForceDefaultLicenseUri(true).setLicenseRequestHeaders(authorization(drm))
.setMultiSession(false).build()).build()
player.trackSelectionParameters = player.trackSelectionParameters.buildUpon()
.setMaxVideoSize(Int.MAX_VALUE, maximumHeight).setPreferredAudioLanguages("en")
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true).build()
player.setMediaItem(mediaItem); player.prepare(); player.play()
status.text = "Native Media3 · protected playback"
}.onFailure { status.text = "Invalid session contract. Check your backend."; releaseSession() }
}.onFailure { if (!closed.get()) status.text = "Authorization failed. Check login, Content ID, policy, and network." }
}
}
}
private fun releaseSession() { activeSession?.let(::workerRelease); activeSession = null }
private fun workerRelease(session: JSONObject) {
// A bounded release may finish after Activity destruction, including a
// session response that arrived after the user left the screen.
Thread({ runCatching {
val release = session.getJSONObject("release")
val headers = if (release.has("authorization")) authorization(release)
else authorization(session.getJSONObject("drm"))
post(release.getString("url"), "", headers)
} }, "drmx-native-release").start()
}
private fun stopPlayback() { generation++; player.stop(); player.clearMediaItems(); releaseSession() }
override fun onStop() { stopPlayback(); super.onStop() }
override fun onDestroy() { closed.set(true); stopPlayback(); player.release(); worker.shutdown(); super.onDestroy() }
companion object {
fun https(url: String): String {
val uri = URI(url); require(uri.scheme == "https" && uri.host != null && uri.userInfo == null)
return url
}
fun authorization(drm: JSONObject): Map<String, String> {
val auth = drm.optJSONObject("authorization")
val name = auth?.getString("headerName") ?: "Authorization"
val scheme = auth?.getString("scheme") ?: drm.optString("tokenType", "Bearer")
val value = auth?.getString("value") ?: drm.getString("drmLicenseToken")
require(name.matches(Regex("^[A-Za-z0-9-]{1,64}$")) && scheme.matches(Regex("^[A-Za-z][A-Za-z0-9._-]{0,31}$")))
require(value.isNotBlank() && !value.contains('\r') && !value.contains('\n'))
return mapOf(name to "$scheme $value")
}
fun post(url: String, body: String, headers: Map<String, String>): String {
val connection = URI(https(url)).toURL().openConnection() as HttpURLConnection
try {
connection.requestMethod = "POST"; connection.instanceFollowRedirects = false
connection.connectTimeout = 5000; connection.readTimeout = 15000; connection.doOutput = true
connection.setRequestProperty("Content-Type", "application/json")
headers.forEach { (name, value) -> connection.setRequestProperty(name, value) }
connection.outputStream.use { it.write(body.toByteArray(Charsets.UTF_8)) }
require(connection.responseCode in 200..299) { "Customer request rejected" }
return connection.inputStream.use { stream ->
val output = java.io.ByteArrayOutputStream(); val buffer = ByteArray(8192)
while (true) {
val count = stream.read(buffer); if (count < 0) break
require(output.size() + count <= 1_048_576); output.write(buffer, 0, count)
}
output.toString("UTF-8")
}
} finally { connection.disconnect() }
}
}
}
本地离线许可证,使用 OfflineLicenseHelper.downloadLicense,坚持其不透明的密钥集 ID 私下恢复 MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID).setKeySetId(id)。以下编译的辅助工具包括购置、时间检查、更新、发布和下载/回放项目。 不是一个完整的下载程序: 您仍然拥有缓存, 下载服务, 进度, 元数据, 用户/ 设备绑定和信任的操作授权 。
原生的耐用许可证助手 (Kotlin, no DRM-X 依赖性)
package com.haihaisoft.drmx.samples.nativeplayer
import android.content.Context
import android.util.Base64
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.StreamKey
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.exoplayer.dash.DashUtil
import androidx.media3.exoplayer.drm.DrmSessionEventListener
import androidx.media3.exoplayer.drm.OfflineLicenseHelper
import androidx.media3.exoplayer.offline.DownloadRequest
import org.json.JSONObject
/** Native Media3 alternative: no DRM-X AAR. Call blocking operations on a worker.
* Application owns downloads/cache, user/device entitlement, progress and lifecycle.
* Never persist the session, request headers, tokens, challenges or license bytes.
*/
@UnstableApi
class NativeOfflineLicense(context: Context) {
private val storage = context.getSharedPreferences("native.offline.keysets", Context.MODE_PRIVATE)
private val upstream = DefaultHttpDataSource.Factory().setConnectTimeoutMs(10000).setReadTimeoutMs(20000)
// session is the validated response from YOUR offline-license-session endpoint.
fun acquire(session: JSONObject): ByteArray {
validate(session)
val contentId = session.getString("contentId")
require(!storage.contains(contentId)) { "Renew or release the stored license before acquiring again" }
val source = upstream.createDataSource()
val manifest = DashUtil.loadManifest(source, android.net.Uri.parse(MainActivity.https(session.getString("manifestUrl"))))
val format = DashUtil.loadFormatWithDrmInitData(upstream.createDataSource(), manifest.getPeriod(0))
?: error("Missing Widevine initialization data")
val helper = helper(session)
try {
val id = helper.downloadLicense(format)
save(contentId, id)
return id
} finally { helper.release() }
}
fun renew(contentId: String, freshSession: JSONObject) {
validate(freshSession); require(freshSession.getString("contentId") == contentId)
val helper = helper(freshSession)
try { save(contentId, helper.renewLicense(keySet(contentId))) } finally { helper.release() }
}
// Local CDM check only. No token or network license URI is used.
fun remainingSeconds(contentId: String): Pair<Long, Long> {
val config = MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID).build()
val helper = OfflineLicenseHelper.newWidevineInstance(config, upstream, DrmSessionEventListener.EventDispatcher())
try {
val remaining = helper.getLicenseDurationRemainingSec(keySet(contentId))
return Pair(remaining.first, remaining.second)
} finally { helper.release() }
}
fun release(contentId: String, freshSession: JSONObject) {
validate(freshSession); require(freshSession.getString("contentId") == contentId)
val helper = helper(freshSession)
try {
helper.releaseLicense(keySet(contentId))
check(storage.edit().remove(contentId).commit())
} finally { helper.release() }
}
fun downloadRequest(contentId: String, downloadId: String, manifestUrl: String, streamKeys: List<StreamKey>): DownloadRequest {
require(streamKeys.isNotEmpty()) { "Select policy-compliant video, audio and subtitle tracks first" }
return DownloadRequest.Builder(downloadId, android.net.Uri.parse(MainActivity.https(manifestUrl)))
.setMimeType("application/dash+xml").setStreamKeys(streamKeys).setKeySetId(keySet(contentId)).build()
}
fun mediaItem(contentId: String, manifestUrl: String, streamKeys: List<StreamKey>): MediaItem = MediaItem.Builder()
.setUri(MainActivity.https(manifestUrl)).setMimeType("application/dash+xml").setStreamKeys(streamKeys)
.setDrmConfiguration(MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID).setKeySetId(keySet(contentId)).build()).build()
private fun helper(session: JSONObject): OfflineLicenseHelper {
val drm = session.getJSONObject("drm")
// Use HTTP license headers, NOT Widevine optional key-request parameters.
val config = MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
.setLicenseUri(MainActivity.https(drm.getString("licenseServerUrl")))
.setForceDefaultLicenseUri(true).setLicenseRequestHeaders(MainActivity.authorization(drm)).build()
return OfflineLicenseHelper.newWidevineInstance(config, upstream, DrmSessionEventListener.EventDispatcher())
}
private fun validate(session: JSONObject) {
require(session.getInt("contractVersion") == 1 && session.getString("playbackMode") == "offline")
require(session.getString("drmSystem") == "widevine" && session.getString("contentType") == "vod")
require(session.getJSONObject("policy").getBoolean("persistent"))
}
private fun keySet(contentId: String): ByteArray = Base64.decode(storage.getString(contentId, null)
?: error("No device-bound license for this content"), Base64.NO_WRAP)
private fun save(contentId: String, id: ByteArray) {
check(storage.edit().putString(contentId, Base64.encodeToString(id, Base64.NO_WRAP)).commit())
}
}
在调用此低级帮助器前, 请验证新后端响应符合请求的Content ID, 操作, 签名的用户/ 设备以及授权有效期 。 将一个稳定的离线LicenseId 和可信任的 explete/ track 元数据与密钥集ID 一起使用。 运行屏蔽帮助器调用工人线索 。 对于许可证HTTP 头使用DRM 配置 setLicenseRequestHeaders ;Widevine 可选的密钥请求参数是不同的地图 。
应采用新的整合办法 Android页:1 Media3 DRM 支助. 遗产 com.google.android.exoplayer2 项目应当遵循 官方移徙指南;不混合遗产和 AndroidX类或假设样本对照旧版本编译不变.
9. 检查、更新、发布和切换语言#
这些是单独的动作。 在活动下线 DRM 会话发布后一次运行一次 : 用 DrmXPlayer 使用player.stopForOfflineLicenseOperation { /* one operation below */ } , 或者先释放已有的本地 Media3 播放器 。 在播放时不要检查许可证 。 对显示持续时间, 检查是可选的; 它不能阻止过期许可证的续签 。
offlineManager.checkLicense(contentId) { result ->
result.onSuccess { license ->
if (license.state == DrmXOfflineLicenseState.EXPIRED) {
// Require renewal online before offering offline playback.
}
}
}
offlineManager.renewLicense(offlineEndpoint, contentId, capabilities, customerHeaders) {
/* update duration and UI after success; show retry on failure */
}
// Only when the customer requests deletion/release:
offlineManager.releaseLicense(offlineEndpoint, contentId, capabilities, customerHeaders) {
/* after success, remove the corresponding encrypted download */
}
getStoredLicense 读取元数据; checkLicense 本地权威查询 CDM 持续时间。 更新/ 释放需要您的后端的连接和新的操作授权。 删除媒体不会释放许可证。 断开时, 排队释放下一次连接, 而不是报告服务器已经确认。 当所有者被摧毁时关闭管理器 。
// Use player.media3Player for the DRM-X wrapper; nativePlayer for plain Media3.
nativePlayer.trackSelectionParameters = nativePlayer.trackSelectionParameters
.buildUpon()
.setPreferredAudioLanguages("en") // Chinese: "zh-Hans", "zh"
.setPreferredTextLanguages("en") // choose a packaged/downloaded language
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false) // true = subtitles Off
.build()
从实际可用的音轨中构建选择并禁用无法获取的选择。 优先选择不能创建加密输出包中缺失的英语音轨。 离线用户只能选择下载的语言。 在整个屏幕旋转过程中保留许可证/ 缓存状态; 完整屏幕是一个视图/ 窗口的更改, 而不是获取另一个许可证的理由 。
10. 实施离线授权终点#
app posts contentId,contentType: "vod",drmSystem: "widevine",playbackMode: "offline",operation: "acquire" | "renew" | "release",offlineLicenseId(初始获取的无效)和真实的platformCapabilities. 校验是否延长/释放ID属于经认证的用户,设备及内容. 选择策略服务器侧,发布新的DRM License Token并返回匹配合同 :
{
"contractVersion": 1, "playbackMode": "offline",
"offlineLicenseId": "YOUR-STABLE-USER-DEVICE-CONTENT-RECORD",
"contentId": "YOUR-PUBLISHED-CONTENT-ID", "contentType": "vod",
"drmSystem": "widevine",
"manifestUrl": "https://media.customer.example/title/manifest.mpd",
"authorizationExpiresAt": "YOUR-FRESH-UTC-EXPIRY",
"drm": {
"system": "widevine",
"licenseServerUrl": "https://license6.drm-x.com/v1/licenses/widevine",
"integrationMode": "direct-token",
"drmLicenseToken": "SHORT-LIVED-OPERATION-AUTHORIZATION",
"tokenType": "Bearer"
},
"policy": {
"persistent": true, "renewAllowed": true, "maximumHeight": 1080,
"allowedTrackTypes": ["AUDIO", "SD", "HD"],
"licenseDurationSeconds": 604800, "rentalDurationSeconds": 259200,
"playbackDurationSeconds": 172800, "renewalDurationSeconds": 604800
}
}
这些是解释性占位符, 不是可重复使用的许可响应 。 返回实际操作中的新值 。 SDK 也支持 integrationMode: "token-proxy" 使用 drm.authorization 描述符 。 返回 Cache-Control: no-store ; 永远不要登录成功的授权机构 。 请参看可下载的 SDK OFFLINE_INTEGRATION.md 和 PHP 样本的离线端点, 以便进行具体的请求处理 。
11. 测试您的装置和故障排除#
Widevine 供给和中国网络测试
提供在内容许可证获取前获得设备证书。 从预览9 , 电话演示通过认证客户HTTPS 中继器发送到 Google CDM 中继器的签名请求。 所签名的响应返回不变。 当电话无法直接到达 Google 中继器服务时, 这会有所帮助; 客户中继器仍然必须能够到达 。
提供 2.0 使用工厂密钥框。 提供 4. 0 使用不同的设备证书模型。 这些版本与 L1/L3 安全级别,以及 播放器 不切换它们。已经提供了 L3 手机可以在另一部手机等待设备提供时播放。这里审查的公共官方来源不能确定中国仅支持2.0。请参见 NXP设备集成指南 和 AndroidB. 提供条款的执行.
预览10 错误报告区分直接运输或中继运输、超时/DNS/TLS/HTTP 失败, HTTP 状态和时间。 provisioningModel: BootCertificateChain 显示 AOSP 4.0型号; unknown 或 减 减 other 没有建立 2. 0。 PHP 日志区分拒绝的输入和谷歌上游故障。报告不包括签名的有效载荷、 URL, 凭据 和独特的设备标识符。
更新已安装的APK, 重试流线和在受影响的网络上下载一次, 然后保留复制日志和大约时间。 保留现有的下载和许可证。 中继无法修复被设备/ Google 拒绝的Widevine 或凭据 。 需要在受影响的电话上进行真正的接收 。
从预览 8, 本地 L2/L3 或未知级别 Widevine 回放仅限于 SD 解码前( 最多576p) 。 Android 通过您的后端实现能力:会话 API 缩窄软件 Widevine 音频/音频多键许可证SD 键。 PHP 样本保存这些字段,并封顶离线请求。 SD 引渡和允许软件的政策 DRM ; 仅硬件和统一的单键安全规则保持不变。设置使用半透明面板而不淡化视频。 cryptoErrorCode 包含在安全错误报告中。
| 测试/症状 | 预期行为/ 下一次检查 |
|---|---|
| SDK 下载返回 403 | 选择一个拥有活跃的专业或企业订阅的组织。 计划测试后升级不会解锁SDK 。 |
| 第401次会议/第403次会议 | 请检查access-date=中的日期值 (帮助) 登录、权限、环境、出版的Content ID和政策 永远不要绕过认证. |
| 离线失败, 但在线工作 | 请检查CDM 持续时间, 完成的下载, 缓存位置, 流密钥和所有必需的许可密钥 。 |
| 英语/ 缺少中文 | 检查包语言元数据,实际音轨和离线语言选择. |
| L1,HDCP 或解码器失败 | 检查物理硬件、 提供、 解码器和输出要求。 能力字符串不是认证 。 |
| 重启、旋转、升级 | 持续的许可证和缓存在支持的生命周期变化中幸存而无需重复获取。备份不能将许可证转移到新设备。 |
| 飞机模式 | 也禁用Wi-Fi和移动数据; 重新启动应用程序, 本地播放, 并切换所承诺的每一种语言 。 |
| 过期、 存储压力、 账户开关 | 显示清晰的不可用/ 更新状态, 恢复不完整的介质, 并强制用户/ 设备访问而不泄露另一个用户的离线目录 。 |
每一个支持的电话,平板电脑, Android TV/Google TV, Fire TV 或嵌入 Android 模式 Widevine,安全解码,解码器/分辨率, HDCP,外部显示、时钟更改和长时间断开。不是每个 Android 设备有相同的 DRM 好莱坞的内容需要权利持有人的保护和装置要求,而不仅仅是成功的解码.
Universal Player 播放列表和共享控件#
Android 预览 11 和 以后的评价 apps 和 Android/ apple 开发源共享 video President/Play-Pause/Next,CC 设置和全屏控制, 在播放时隐藏两秒。 Android 播放列表菜单不再包括“ 播放下一步 ” 。 选定的洗牌/ 重复设置仍然突出, 并贯穿整个重新启动。 关闭后, 播放列表顺序会随所保存的播放列表顺序而停止在最后的视频中 。 拖动手柄重排项目而不重新启动当前视频 。 保存的顺序也适用于下载的播放列表 。 这些最新的源 APIs 尚未包含在 Console 的预览 10SDK 捆绑中 。
提供同一客户/浏览器 storageNamespace 改为: DrmXPlayer 和 playerView.storageNamespace 在设置播放列表之前。 VOD 定期保存进度,并暂停/停止,然后为此恢复 Content ID 在流媒体播放或下载回放中。实时回放不使用已保存 VOD 位置。 已完成的下载在没有网络媒体源的情况下保留同样的shuffle/repeat/resume行为。 此设备本地状态不会在设备之间同步进度 。
Web 和 Android 版本使用一个带有紧凑蓝色控件的一致的深色设置面板。 播放列表支持下一个/ 先前的, 洗牌的, 重复一个/ 全部, 并且可以自动取消。 每个项目都使用新的授权播放请求。 章节和记录元数据来自您的内容目录 。
回放速度、睡眠计时器、标题、允许的质量、音频语言、会话书签和 A-B 环绕是可用的。Web 增加了键盘快捷键、可选的查看器扫描本地复写、记录搜索和支持的浏览器 PiP 。 Android 保留本地语 Widevine 离线下载和更新. Casting和交叉设备进度需要单独的应用程序集成.
专业和企业标识设置
品牌保留在视频表面之外。 对授权的标志、 产品信息和 播放器 版本的开放设置 播放器 。 Web 也通过右键点击提供此信息。 公用电话演示包括 WayMaker, Goodness of God, Fresh Oil 和 4K Test Video; 每个选择都获得自己的授权 。
设定 playerBranding 在您信任的后端中发送到 {"mode":"hidden"} 或 减 减 {"mode":"custom","logoUrl":"https://your-site.example/logo.png","label":"Your brand"}。该平台检查活跃的专业或企业订阅。其他计划使用 {"mode":"default"};未经授权的自定义/隐藏请求返回 403. 这与安全水印是分开的。
那个... PHP 下载包括可编辑播放列表和品牌配置。 Android 客户 ZIP 包含播放列表召回示例。将播放列表的授权保留在您的后端; 永不嵌入 API 客户端 凭据 任意一个 播放器.
DRM-X 6.0