1/* 2 * Copyright (c) 2024 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import drm from '@ohos.multimedia.drm' 17import media from '@ohos.multimedia.media' 18import DrmConstants, { KeySessionEvents } from '../common/constants/DrmConstants' 19import Logger from './Logger' 20import TypeConversion from './TypeConversion' 21import HttpUtil from './HttpUtil' 22import KVManagerUtil from './KVManagerUtil' 23 24export default class DrmController { 25 private TAG: string = 'DrmController' 26 private mediaKeySystem: drm.MediaKeySystem = {} as drm.MediaKeySystem 27 private mediaKeySession: drm.MediaKeySession = {} as drm.MediaKeySession 28 private drmInfoArr: drm.MediaKeySystemInfo[] = [] 29 private solutionName: string = '' 30 private licenseInfo: string = '' 31 private drmUrl: string = '' 32 private licenseType: number = 1 33 private svp: boolean = false 34 private releaseCallback: () => void = () => { 35 } 36 private licenseInfoCallback: () => void = () => { 37 } 38 39 /** 40 * 判断系统是否支持 drm 能力 41 */ 42 isDrmSupported(): boolean { 43 if (drm.isMediaKeySystemSupported(DrmConstants.WISEPLAY_DRM_NAME)) { 44 this.solutionName = DrmConstants.WISEPLAY_DRM_NAME 45 return true 46 } else if (drm.isMediaKeySystemSupported(DrmConstants.CLEAR_PLAY_DRM_NAME)) { 47 this.solutionName = DrmConstants.CLEAR_PLAY_DRM_NAME 48 return true 49 } 50 return false 51 } 52 53 /** 54 * 创建 MediaKeySystem、KeySession 实例 55 */ 56 createDrmSystem() { 57 this.mediaKeySystem = drm.createMediaKeySystem(this.solutionName) 58 if (!this.mediaKeySystem) { 59 Logger.error(this.TAG, 'getMediaKeySystem fail!') 60 return 61 } 62 Logger.info(this.TAG, 'MediaKeySystem has been created.') 63 64 // 若无证书,此接口会返回失败 65 this.mediaKeySession = this.mediaKeySystem 66 .createMediaKeySession(drm.ContentProtectionLevel.CONTENT_PROTECTION_LEVEL_HW_CRYPTO) 67 if (this.mediaKeySession) { 68 Logger.info(this.TAG, 'mediaKeySession has been created.') 69 this.svp = this.mediaKeySession.requireSecureDecoderModule('video/avc') 70 Logger.info(this.TAG, ' this.svp: ' + this.svp) 71 this.setDrmCallback() 72 } else { 73 Logger.error(this.TAG, 'createKeySession fail!') 74 this.getProvision() 75 } 76 } 77 78 /** 79 * 注册 drm 相关回调函数 80 */ 81 setDrmCallback() { 82 if (!this.mediaKeySession) { 83 Logger.error(this.TAG, 'this.mediaKeySession is undefined!') 84 return 85 } 86 Logger.info(this.TAG, 'Start to set callback') 87 // renew上报 重新获取license 88 this.mediaKeySession.on(KeySessionEvents.KEY_REQUIRED, async (eventInfo: drm.EventInfo) => { 89 Logger.info(this.TAG, 'keyRequired callback success.') 90 Logger.info(this.TAG, `info: ${TypeConversion.byteToString(eventInfo.info)}, extraInfo: ${eventInfo.extraInfo}`) 91 92 this.renew() 93 this.getLicenseInfo() 94 }) 95 // 密钥过期上报 播放失败 96 this.mediaKeySession.on(KeySessionEvents.KEY_EXPIRED, (eventInfo: drm.EventInfo) => { 97 Logger.info(this.TAG, 'keyExpired callback success.') 98 Logger.info(this.TAG, `info: ${TypeConversion.byteToString(eventInfo.info)}, extraInfo: ${eventInfo.extraInfo}`) 99 100 this.getLicenseInfo() 101 this.releaseCallback() 102 }) 103 // license过期时长上报 license响应交给底层时触发 104 this.mediaKeySession.on(KeySessionEvents.EXPIRATION_UPDATE, (eventInfo: drm.EventInfo) => { 105 Logger.info(this.TAG, 'expirationUpdate callback success.') 106 Logger.info(this.TAG, `info: ${TypeConversion.byteToString(eventInfo.info)}, extraInfo: ${eventInfo.extraInfo}`) 107 }) 108 } 109 110 /** 111 * 注销 drm 相关回调函数 112 */ 113 destroyDrmCallback() { 114 if (!this.mediaKeySession) { 115 Logger.error(this.TAG, 'this.mediaKeySession is undefined!') 116 return 117 } 118 this.mediaKeySession.off(KeySessionEvents.KEY_REQUIRED) 119 this.mediaKeySession.off(KeySessionEvents.KEY_EXPIRED) 120 this.mediaKeySession.off(KeySessionEvents.EXPIRATION_UPDATE) 121 } 122 123 /** 124 * 获取 Provision 125 */ 126 async getProvision() { 127 if (!this.mediaKeySystem) { 128 Logger.error(this.TAG, 'getProvision: this.mediaKeySystem is undefined!') 129 return 130 } 131 // Provision Request 132 let provisionRequestData: drm.ProvisionRequest = await this.mediaKeySystem.generateKeySystemRequest() 133 let provisionRequestStr: string = TypeConversion.byteToString(provisionRequestData.data) 134 Logger.info(this.TAG, 'ProvisionRequest[' + provisionRequestStr.length + ']:' + provisionRequestStr) 135 136 // Provision Response 137 try { 138 let provisionResponseStr: string = await HttpUtil.getDrmResponse('https://drmkit.hwcloudtest.cn:8080/provision/v1/wiseplay', provisionRequestStr) 139 Logger.info(this.TAG, 'provisionResponse[' + provisionResponseStr.length + ']:' + provisionResponseStr) 140 let provisionRequestByte: Uint8Array = TypeConversion.stringToByte(provisionResponseStr) 141 await this.mediaKeySystem.processKeySystemResponse(provisionRequestByte) 142 Logger.info(this.TAG, 'get provision success.') 143 } catch (e) { 144 Logger.info(this.TAG, 'error [' + JSON.stringify(e) + ']') 145 } 146 } 147 148 /** 149 * 获取license 150 * @param drmInfoArr drm信息参数 151 * @param drmUrl 片源地址 152 * @param drmLicenseUrl license请求地址 153 * @param licenseType license类型, 1 在线, 0 离线 154 * @returns 155 */ 156 async getLicense(drmInfoArr: drm.MediaKeySystemInfo[], drmUrl: string, drmLicenseUrl: string, licenseType: number): Promise<boolean> { 157 if (!this.mediaKeySession) { 158 Logger.error(this.TAG, 'this.mediaKeySession is undefined!') 159 return false 160 } 161 162 let isSuccess: boolean = true 163 for (let i = 0; i < drmInfoArr.length; i++) { 164 Logger.info(this.TAG, 'drmInfoArr - uuid: ' + drmInfoArr[i].uuid) 165 Logger.info(this.TAG, 'drmInfoArr - pssh: ' + drmInfoArr[i].pssh) 166 167 // license Request 168 const optionsData: drm.OptionsData[] = [{ 169 name: 'optionalDataName', 170 value: 'optionalDataValue' 171 }] 172 const uint8pssh: Uint8Array = new Uint8Array(drmInfoArr[i].pssh) 173 const licenseRequestData: drm.MediaKeyRequest = await this.mediaKeySession.generateMediaKeyRequest('video/mp4', uint8pssh, licenseType, optionsData) 174 let licenseRequestStr: string = TypeConversion.byteToString(licenseRequestData.data) 175 Logger.info(this.TAG, 'licenseRequestStr[' + licenseRequestStr.length + ']:' + licenseRequestStr) 176 177 // license Response 178 let licenseResponseStr: string = await HttpUtil.getDrmResponse(drmLicenseUrl, licenseRequestStr) 179 Logger.info(this.TAG, 'licenseResponseStr[' + licenseResponseStr.length + ']:' + licenseResponseStr) 180 181 // 请求异常 182 if (licenseResponseStr === 'defaultStr') { 183 isSuccess = false 184 } else { 185 // 请求地址或传参错误时返回的result不是正确的license response 186 const licenseResponseObj: Record<string, Object> = JSON.parse(licenseResponseStr) 187 if (!licenseResponseObj.certificateChain) { 188 Logger.error(this.TAG, 'licenseResponse msg: ' + licenseResponseObj?.msg) 189 isSuccess = false 190 } 191 } 192 193 let licenseResponseData: Uint8Array = TypeConversion.stringToByte(licenseResponseStr) 194 // 将响应交给底层(在线license返回空字符串,离线license返回keySetId) 195 try { 196 let KeySetId: Uint8Array = await this.mediaKeySession.processMediaKeyResponse(licenseResponseData) 197 // 若为离线license则存储起来 198 if (licenseType === 0 && KeySetId) { 199 Logger.info(this.TAG, 'KeySetId is: ' + TypeConversion.byteToString(KeySetId)) 200 KVManagerUtil.put(drmUrl, TypeConversion.byteToString(KeySetId)) 201 // 查看已下载的离线Id 202 const keyIds: Uint8Array[] = this.mediaKeySystem.getOfflineMediaKeyIds() 203 keyIds.forEach((item: Uint8Array) => { 204 Logger.info(this.TAG, 'Downloaded offline id: ' + TypeConversion.byteToString(item)) 205 }) 206 } 207 } catch (e) { 208 isSuccess = false 209 Logger.error(this.TAG, `get KeySetId error, message is ${(e as Error).message}`) 210 } 211 } 212 return isSuccess 213 } 214 215 /** 216 * 执行license流程 217 * @param drmUrl 218 * @param drmLicenseUrl 219 * @param drmInfoArr 220 * @param licenseType 221 * @returns 222 */ 223 async executeLicenseProcess(drmUrl: string, drmLicenseUrl: string, drmInfoArr: drm.MediaKeySystemInfo[], licenseType: number): Promise<boolean> { 224 if (!this.mediaKeySession) { 225 Logger.error(this.TAG, 'this.mediaKeySession is undefined!') 226 return false 227 } 228 Logger.info(this.TAG, 'Start to execute license process') 229 230 this.drmUrl = drmUrl 231 this.drmInfoArr = drmInfoArr 232 this.licenseType = licenseType 233 234 if (licenseType === 1) { 235 Logger.info(this.TAG, 'Get onlineLicense') 236 return await this.getLicense(drmInfoArr, drmUrl, drmLicenseUrl, licenseType) 237 } else if (licenseType === 0) { 238 let keyId: string = await KVManagerUtil.get(drmUrl) 239 if (keyId) { // 已存储过,直接使用离线license进行播放 240 Logger.info(this.TAG, `Offlinelicense exist, keyId is ${keyId}`) 241 this.mediaKeySession.restoreOfflineMediaKeys(TypeConversion.stringToByte(keyId)) 242 return true 243 } else { // 若没有存储过,则需要请求license 244 Logger.info(this.TAG, 'Get offlineLicense') 245 return await this.getLicense(drmInfoArr, drmUrl, drmLicenseUrl, licenseType) 246 } 247 } else { 248 Logger.error(this.TAG, 'LicenseType error') 249 return false 250 } 251 } 252 253 /** 254 * 删除离线license 255 * @param drmUrl 256 */ 257 async deleteOfflineLicense(drmUrl: string) { 258 if (!this.mediaKeySession) { 259 Logger.error(this.TAG, 'this.mediaKeySession is undefined!') 260 return 261 } 262 263 let keyId: string = await KVManagerUtil.get(drmUrl) 264 Logger.info(this.TAG, 'KeyId to be delete is: ' + keyId) 265 if (keyId) { 266 let offlineLicenseId = new Uint8Array(TypeConversion.stringToByte(keyId)) 267 let offlineLicenseStatus: drm.OfflineMediaKeyStatus = this.mediaKeySystem.getOfflineMediaKeyStatus(offlineLicenseId) 268 Logger.info(this.TAG, 'getOfflineMediaKeyStatus: ' + offlineLicenseStatus) 269 270 this.mediaKeySystem.clearOfflineMediaKeys(offlineLicenseId) 271 KVManagerUtil.delete(drmUrl) 272 Logger.info(this.TAG, 'Delete offline license success.') 273 } 274 } 275 276 /** 277 * renew 重新获取license 278 */ 279 async renew() { 280 // 获取license信息 281 const mediaKeyStatus: drm.MediaKeyStatus[] = this.mediaKeySession.checkMediaKeyStatus() 282 const licenseInfoMap: Map<string, string> = new Map<string, string>() 283 mediaKeyStatus.forEach((item: drm.MediaKeyStatus) => { 284 licenseInfoMap.set(item.name, item.value) 285 }) 286 Logger.info(this.TAG, `checkMediaKeyStatus: ${JSON.stringify(Array.from(licenseInfoMap.entries()))}`) 287 288 // 获取新url请求license 289 let renewAllowed: boolean = licenseInfoMap.has('RenewAllowed') && (licenseInfoMap.get('RenewAllowed') as string) === 'True' 290 if (renewAllowed) { 291 let oldKeyId: string = await KVManagerUtil.get(this.drmUrl) 292 let renewalServerUrl: string = licenseInfoMap.has('RenewalServerUrl') ? (licenseInfoMap.get('RenewalServerUrl') as string) : '' 293 Logger.info(this.TAG, `RenewalServerUrl: ${renewalServerUrl}`) 294 295 await this.getLicense(this.drmInfoArr, this.drmUrl, renewalServerUrl, this.licenseType) 296 // 离线renew需要删除旧id 297 if (this.licenseType === 0 && oldKeyId) { 298 let offlineLicenseId = new Uint8Array(TypeConversion.stringToByte(oldKeyId)) 299 this.mediaKeySystem.clearOfflineMediaKeys(offlineLicenseId) 300 Logger.info(this.TAG, 'Renew - delete old offline license success.') 301 } 302 } else { 303 Logger.error(this.TAG, 'renewAllowed: ' + renewAllowed) 304 } 305 } 306 307 /** 308 * 将KeySession与AVPlayer实例绑定 309 * @param avPlayer 310 * @param svp 是否需要安全视频通路 311 */ 312 bindInstance(avPlayer: media.AVPlayer) { 313 avPlayer.setDecryptionConfig(this.mediaKeySession, this.svp) 314 Logger.info(this.TAG, 'DecryptConfig has been set.') 315 } 316 317 /** 318 * 获取license信息 319 * @returns 320 */ 321 getLicenseInfo(): string { 322 const mediaKeyStatus: drm.MediaKeyStatus[] = this.mediaKeySession.checkMediaKeyStatus() 323 const licenseInfo: string = JSON.stringify(mediaKeyStatus) 324 let licenseInfoText: string = '' 325 326 if (licenseInfo) { 327 Logger.info(this.TAG, `licenseInfo: ${licenseInfo}`) 328 const licenseInfoArr: drm.MediaKeyStatus[] = JSON.parse(licenseInfo) 329 330 licenseInfoText = 'License Info \n \n' 331 licenseInfoArr.forEach((item: drm.MediaKeyStatus) => { 332 licenseInfoText = licenseInfoText + item.name + ': ' + item.value + '\n' 333 }) 334 335 let licenseType: string = (AppStorage.get('licenseType') as number) === 0 ? 'offline' : 'online' 336 licenseInfoText = licenseInfoText + 'LicenseType: ' + licenseType + '\n' 337 } 338 339 this.licenseInfo = licenseInfo 340 return licenseInfoText 341 } 342 343 /** 344 * 释放资源 345 */ 346 releaseDrm() { 347 if (!this.mediaKeySession) { 348 Logger.error(this.TAG, 'this.mediaKeySession is undefined!') 349 return 350 } 351 try { 352 this.destroyDrmCallback() 353 this.mediaKeySession.destroy() 354 this.mediaKeySystem.destroy() 355 Logger.info(this.TAG, 'mediaKeySession release success.') 356 } catch (err) { 357 Logger.error(this.TAG, `Failed to destroy Drm mediaKey. Error: [${err}]`) 358 } 359 } 360 361 /** 362 * 释放avplayer播放资源 363 * @param func 364 */ 365 getReleaseCallback(func: () => void) { 366 this.releaseCallback = func 367 } 368}