Skip to content

Commit

Permalink
feat: add endpoint for generate urls
Browse files Browse the repository at this point in the history
  • Loading branch information
CCharlieLi committed Feb 22, 2021
1 parent d03c423 commit 59a345b
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 11 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
This is a library for integrating with AliCloud live-streaming platform - [Apsara](https://www.alibabacloud.com/help/doc-detail/29951.htm?spm=a2c63.p38356.b99.2.2c0d56a2C7EHql).

Integration progress:

- [x] [get domains](https://www.alibabacloud.com/help/doc-detail/88332.htm?spm=a2c63.p38356.b99.143.17872c80zDTOBs)
- [x] generate ingest/streaming url with signature
- [ ] TBD

### How to use

```js
import { Apsara, ApsaraDomainsData } from 'alicloud-apsara'

Expand Down Expand Up @@ -42,6 +45,7 @@ const domainsData: ApsaraDomainsData = await apsara.getDomains()
### Specs

#### Apsara(options [,logger])

- **options**, required
- `accessKeyId`: string, required
- `accessKeySecret`: string, required
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "alicloud-apsara",
"version": "0.0.1",
"version": "0.0.2",
"description": "A library for AliCloud live-streaming platform - Apsara",
"keywords": [
"AliCloud",
Expand Down Expand Up @@ -52,6 +52,7 @@
"dependencies": {
"axios": "^0.21.1",
"crypto": "^1.0.1",
"date-fns": "^2.17.0",
"http-errors": "^1.8.0",
"uuid": "^8.3.2"
},
Expand Down
117 changes: 107 additions & 10 deletions src/Apsara.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import axios, { AxiosInstance, AxiosResponse } from 'axios'
import { createHmac } from 'crypto'
import crypto, { createHmac } from 'crypto'
import { addSeconds } from 'date-fns'
import createHttpError from 'http-errors'
import { escape } from 'querystring'
import { URL } from 'url'
import { v4 as uuidv4 } from 'uuid'

import { API_VERSION, BASE_URL, FORMAT, SIGNATURE_METHOD, SIGNATURE_VERSION, TIMEOUT } from './constants'
import { ApsaraDomainsData } from './data/ApsaraDomainsData'
import { ApsaraErrorData } from './data/ApsaraErrorData'
import { AsparaOptions } from './data/ApsaraOptions'
import { ApsaraParams } from './data/ApsaraParams'
import { Logger } from './data/Logger'
import {
ApsaraDomainsData,
ApsaraErrorData,
ApsaraIngestUrlParams,
ApsaraParams,
ApsaraProtocal,
ApsaraStreamingUrlParams,
ApsaraUrlParams,
AsparaOptions,
Logger
} from './data'

export class Apsara {
private options: AsparaOptions
Expand Down Expand Up @@ -37,16 +45,70 @@ export class Apsara {
*/
async getDomains(): Promise<ApsaraDomainsData> {
this.logger?.info(`Get domains of current Apsara account`)
return this.request<ApsaraDomainsData>({ Action: 'DescribeLiveUserDomains' })
return this._request<ApsaraDomainsData>({ Action: 'DescribeLiveUserDomains' })
}

/**
* Create ingest URL with signature
*/
getIngestUrl({ domain, appName, streamName, expiredIn, key }: ApsaraIngestUrlParams): Promise<URL> {
const protocol = 'rtmp:'
const extension = this._getFileExtension('rtmp' as ApsaraProtocal)
return this._generateUrl({ protocol, domain, appName, streamName, extension, expiredIn, key })
}

/**
* Create streaming URL with signature
*/
getVideoStreamingUrl({
domain,
appName,
streamName,
expiredIn,
key,
format,
isSecure
}: ApsaraStreamingUrlParams): Promise<URL> {
const protocol = this._getProtocol(format, isSecure)
const extension = this._getFileExtension(format)
return this._generateUrl({ protocol, domain, appName, streamName, extension, expiredIn, key })
}

// Private

/**
* Generate Apsara ingest/streaming URL with signature
* See https://help.aliyun.com/document_detail/199349.html?spm=a2c4g.11186623.2.4.36005f12Jui3YZ
* @param protocol
* @param domain
* @param appName
* @param streamName
* @param extension
* @param expiredIn seconds
* @param key
*/
private async _generateUrl({
protocol,
domain,
appName,
streamName,
extension,
expiredIn,
key
}: ApsaraUrlParams): Promise<URL> {
const unixTimestamp = Math.trunc(addSeconds(new Date(), expiredIn).getTime() / 1000)
const signature = `${appName}/${streamName}/${extension}-${unixTimestamp}-0-0-${key}`
const hashedSignature = crypto.createHash('md5').update(signature).digest('hex')
return new URL(
`${protocol}//${domain}/${appName}/${streamName}${extension}?auth_key=${unixTimestamp}-0-0-${hashedSignature}`
)
}

/**
* Make HTTP request to Apsara open API
* @param {object} params
*/
private async request<T>(params: Record<string, string>): Promise<T> {
private async _request<T>(params: Record<string, string>): Promise<T> {
this.logger?.info(`Send request to Apsara with payload ${JSON.stringify(params)}`)

// create payload
Expand All @@ -60,7 +122,7 @@ export class Apsara {
SignatureNonce: uuidv4(),
Timestamp: new Date().toISOString()
}
const signature: string = this.generateSignature({ ...commonParams, ...params }, method)
const signature: string = this._generateSignature({ ...commonParams, ...params }, method)

// init request
let response: AxiosResponse<T>
Expand Down Expand Up @@ -98,7 +160,7 @@ export class Apsara {
* @param {object} payload
* @param {string} method
*/
private generateSignature(payload: any, method: string): string {
private _generateSignature(payload: any, method: string): string {
// sort params
const paramsStr = Object.keys(payload)
.map(key => (key === 'Timestamp' ? `${key}=${encodeURIComponent(payload[key])}` : `${key}=${payload[key]}`))
Expand All @@ -115,4 +177,39 @@ export class Apsara {
)
.digest('base64')
}

/**
* Get URL protocal based on given format
* @param format
* @param isSecure
*/
private _getProtocol(format: ApsaraProtocal, isSecure = false): string {
switch (format) {
case 'rtmp':
return 'rtmp:'
case 'udp':
return 'udp:'
case 'flv':
case 'm3u8':
default:
return isSecure ? 'https:' : 'http:'
}
}

/**
* Get URL file extension based on given format
* @param format
*/
private _getFileExtension(format: ApsaraProtocal): string {
switch (format) {
case 'flv':
return `.flv`
case 'm3u8':
return `.m3u8`
case 'rtmp':
case 'udp':
default:
return ''
}
}
}
7 changes: 7 additions & 0 deletions src/data/ApsaraIngestUrlParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface ApsaraIngestUrlParams {
domain: string
appName: string
streamName: string
expiredIn: number
key: string
}
1 change: 1 addition & 0 deletions src/data/ApsaraProtocal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type ApsaraProtocal = 'rtmp' | 'flv' | 'm3u8' | 'udp'
11 changes: 11 additions & 0 deletions src/data/ApsaraStreamingUrlParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ApsaraProtocal } from './ApsaraProtocal'

export interface ApsaraStreamingUrlParams {
domain: string
appName: string
streamName: string
expiredIn: number
key: string
format: ApsaraProtocal
isSecure: boolean
}
9 changes: 9 additions & 0 deletions src/data/ApsaraUrlParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface ApsaraUrlParams {
protocol: string
domain: string
appName: string
streamName: string
extension: string
expiredIn: number
key: string
}
4 changes: 4 additions & 0 deletions src/data/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
export * from './ApsaraDomainsData'
export * from './ApsaraErrorData'
export * from './ApsaraIngestUrlParams'
export * from './ApsaraOptions'
export * from './ApsaraParams'
export * from './ApsaraProtocal'
export * from './ApsaraStreamingUrlParams'
export * from './ApsaraUrlParams'
export * from './Logger'
33 changes: 33 additions & 0 deletions src/test.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import nock from 'nock'
import { URL } from 'url'

import { Apsara } from './Apsara'
import { BASE_URL } from './constants'
Expand Down Expand Up @@ -83,4 +84,36 @@ describe('Unit test', () => {
}
})
})

describe('Get Url', () => {
it('should get ingest url successfully', async () => {
const url: URL = await apsara.getIngestUrl({
domain: 'eko.com',
appName: 'testApp',
streamName: 'testStream',
expiredIn: 3600,
key: 'testKey'
})

expect(url.protocol).toBe('rtmp:')
expect(url.host).toBe('eko.com')
expect(url.pathname).toBe('/testApp/testStream')
})

it('should get streaming url successfully', async () => {
const url: URL = await apsara.getVideoStreamingUrl({
domain: 'eko.com',
appName: 'testApp',
streamName: 'testStream',
expiredIn: 3600,
key: 'testKey',
format: 'm3u8',
isSecure: true
})

expect(url.protocol).toBe('https:')
expect(url.host).toBe('eko.com')
expect(url.pathname).toBe('/testApp/testStream.m3u8')
})
})
})

0 comments on commit 59a345b

Please sign in to comment.