• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}