Skip to content
This repository has been archived by the owner on Sep 10, 2021. It is now read-only.

Commit

Permalink
feat: add methods for starting and ending live streams
Browse files Browse the repository at this point in the history
  • Loading branch information
szdc committed Nov 25, 2018
1 parent b84535b commit edfceec
Show file tree
Hide file tree
Showing 7 changed files with 349 additions and 3 deletions.
88 changes: 85 additions & 3 deletions README.md
Expand Up @@ -72,6 +72,10 @@ const api = new TikTokAPI(params, { signURL });
* [.joinLiveStream(id)](#joinlivestreamid)
* [.leaveLiveStream(id)](#leavelivestreamid)
* [.canStartLiveStream()](#canstartlivestream)
* [.startLiveStream(title, [contactsAuthorized])](#startlivestreamtitle-contactsauthorized)
* [.endLiveStream(roomId, streamId)](#endlivestreamroomid-streamid)
* [.createLiveStreamRoom(title, [contactsAuthorized])](#createlivestreamroomtitle-contactsauthorized)
* [.updateLiveStreamStatus(params)](#updatelivestreamstatusparams)

#### .loginWithEmail(email, password)

Expand Down Expand Up @@ -467,7 +471,7 @@ api.joinLiveStream('<room_id>')

```

See the [live stream types](src/types/live-stream.d.ts) for the complete request/response objects.
See the [live stream types](src/types/live-stream.d.ts) for the response data.

#### .leaveLiveStream(id)

Expand All @@ -483,8 +487,6 @@ api.leaveLiveStream('<room_id>')

```

See the [live stream types](src/types/live-stream.d.ts) for the complete request/response objects.

#### .canStartLiveStream()

Determines if the current user is allowed to start a live stream.
Expand All @@ -499,6 +501,86 @@ api.canStartLiveStream()

```

See the [live stream types](src/types/live-stream.d.ts) for the response data.

#### .startLiveStream(title, [contactsAuthorized])

Starts a live stream by calling [`createLiveStreamRoom`](#createlivestreamroomtitle-contactsauthorized)
then [`updateLiveStreamStatus`](#updatelivestreamstatusparams).

Keep note of the `room_id` and `stream_id` properties because you will need them to end the live stream.

The `rtmp_push_url` value can be used with streaming applications such as OBS.

```javascript
api.startLiveStream('title')
.then(res => console.log(res.data.room))
.catch(console.log);

// Outputs:
// { create_time: 1000000000, owner: {...}, stream_url: {...}, title: 'Example', user_count: 1000, ... }

```

See the [live stream types](src/types/live-stream.d.ts) for the response data.

#### .endLiveStream(roomId, streamId)

Ends a live stream.

You **must** call this method to so you are no longer marked as "live" in the app.

```javascript
api.endLiveStream('<room_id>', '<stream_id>')
.then(res => console.log(res.data.status_code))
.catch(console.log);

// Outputs:
// 0

```

#### .createLiveStreamRoom(title, [contactsAuthorized])

Creates a room to host a live stream.

The `rtmp_push_url` value can be used with streaming applications such as OBS.

**Note:** This method only creates the room for the live stream. You'll need to call
[`updateLiveStreamStatus`](#updatelivestreamstatusparams) to mark the stream as started.
See [`startLiveStream`](#startlivestreamtitle-contactsauthorized) for a helper method that makes these calls for you.

```javascript
api.startLiveStream('title')
.then(res => console.log(res.data.room))
.catch(console.log);

// Outputs:
// { create_time: 1000000000, owner: {...}, stream_url: {...}, title: 'Example', user_count: 1000, ... }

```

See the [live stream types](src/types/live-stream.d.ts) for the response data.

#### .updateLiveStreamStatus(params)

Updates the status of a live stream.

```javascript
api.updateLiveStreamStatus({
room_id: '<room_id>',
stream_id: '<stream_id>',
status: LiveStreamStatus.Ended,
reason_no: LiveStreamStatusChangedReason.InitiatedByUser,
})
.then(res => console.log(res.data.status_code))
.catch(console.log);

// Outputs:
// 0

```

See the [live stream types](src/types/live-stream.d.ts) for the complete request/response objects.

## Resources
Expand Down
71 changes: 71 additions & 0 deletions src/index.ts
Expand Up @@ -7,6 +7,7 @@ import { CookieJar } from 'tough-cookie';
import * as API from './types';
import { encryptWithXOR } from './cryptography';
import { FeedType, PullType } from './feed';
import { LiveStreamStatus, LiveStreamStatusChangedReason } from './live-stream';
import { paramsOrder, paramsSerializer, withDefaultListParams } from './params';

export default class TikTokAPI {
Expand Down Expand Up @@ -424,6 +425,76 @@ export default class TikTokAPI {
canStartLiveStream = () =>
this.request.get<API.CanStartLiveStreamResponse | API.BaseResponseData>('aweme/v1/live/podcast/')

/**
* Creates a live stream room and sets the status to started.
*
* @param title
* @param contactsAuthorized
*/
startLiveStream = (title: string, contactsAuthorized = 0) =>
this.createLiveStreamRoom(title, contactsAuthorized)
.then((createRoomRes) => {
if (createRoomRes.data.status_code !== 0) {
throw new Error(`The live stream room could not be created: ${JSON.stringify(createRoomRes.data)}`);
}

const { room } = createRoomRes.data as API.CreateLiveStreamRoomResponse;
return this.updateLiveStreamStatus({
room_id: room.room_id,
stream_id: room.stream_id,
status: LiveStreamStatus.Started,
reason_no: LiveStreamStatusChangedReason.InitiatedByApp,
}).then((updateStatusRes) => {
if (updateStatusRes.data.status_code !== 0) {
throw new Error(`The live stream could not be started: ${JSON.stringify(updateStatusRes.data)}`);
}

return createRoomRes;
});
})

/**
* Ends a live stream.
*
* @param roomId
* @param streamId
*/
endLiveStream = (roomId: string, streamId: string) =>
this.updateLiveStreamStatus({
room_id: roomId,
stream_id: streamId,
status: LiveStreamStatus.Ended,
reason_no: LiveStreamStatusChangedReason.InitiatedByUser,
})

/**
* Creates a room to host a live stream.
*
* @param title
* @param contactsAuthorized
*/
createLiveStreamRoom = (title: string, contactsAuthorized = 0) =>
this.request.post<API.CreateLiveStreamRoomResponse | API.BaseResponseData>('aweme/v1/room/create/', {
params: <API.CreateLiveStreamRoomRequest>{
title,
contacts_authorized: contactsAuthorized,
},
})

/**
* Updates the status of a live stream.
*
* @param params
*/
updateLiveStreamStatus = (params: API.UpdateLiveStreamStatusRequest) =>
this.request.get<API.UpdateLiveStreamStatusResponse>('aweme/v1/room/update/status/', {
params: <API.UpdateLiveStreamStatusRequest>{
status: LiveStreamStatus.Ended,
reason_no: LiveStreamStatusChangedReason.InitiatedByUser,
...params,
},
})

/**
* Transform using JSONBig to store big numbers accurately (e.g. user IDs) as strings.
*
Expand Down
10 changes: 10 additions & 0 deletions src/live-stream.ts
@@ -0,0 +1,10 @@
export enum LiveStreamStatus {
Created = 1,
Started = 2,
Ended = 4,
}

export enum LiveStreamStatusChangedReason {
InitiatedByApp = 0,
InitiatedByUser = 1,
}
33 changes: 33 additions & 0 deletions src/types/live-stream.d.ts
@@ -1,5 +1,6 @@
import { BaseResponseData } from './request';
import { CommonUserDetails } from './user';
import { LiveStreamStatus, LiveStreamStatusChangedReason } from '../live-stream';

export interface LiveStreamRequest {
/** The ID of the live stream to join or leave */
Expand All @@ -16,6 +17,35 @@ export interface CanStartLiveStreamResponse extends BaseResponseData {
can_be_live_podcast: boolean;
}

export interface CreateLiveStreamRoomRequest {
/** The name of the live stream */
title: string;

/** 1 if the user has given the app permission to read their contacts */
contacts_authorized: 0 | 1;
}

export interface CreateLiveStreamRoomResponse extends BaseResponseData {
room: LiveStream;
}

export interface UpdateLiveStreamStatusRequest {
/** The ID of the stream */
room_id: string;

/** The ID used in the stream URL */
stream_id: string;

/** The status to update to */
status: LiveStreamStatus;

/** Why the status is being updated */
reason_no: LiveStreamStatusChangedReason;
}

export interface UpdateLiveStreamStatusResponse extends BaseResponseData {
}

export interface LiveStream {
/** The timestamp in seconds when the stream was created */
create_time: number;
Expand All @@ -40,6 +70,9 @@ export interface LiveStream {
/** A link to the stream source */
rtmp_pull_url: string;

/** A link used to publish to the stream (only present if you own the live stream) */
rtmp_push_url?: string;

/** The ID used in the stream URL */
sid: string;
};
Expand Down
123 changes: 123 additions & 0 deletions test/live-stream.spec.ts
Expand Up @@ -6,7 +6,9 @@ import TikTokAPI, {
BaseResponseData,
CanStartLiveStreamResponse,
CommonUserDetails,
CreateLiveStreamRoomResponse,
JoinLiveStreamResponse,
UpdateLiveStreamStatusResponse,
} from '../src';
import {
loadTestData,
Expand Down Expand Up @@ -88,3 +90,124 @@ describe('#canStartLiveStream()', () => {
assert.deepStrictEqual(res.data, expected);
});
});

describe('#createLiveStreamRoom()', () => {
it('a successful response should match the interface', async () => {
const api = new TikTokAPI(mockParams, mockConfig);
const mock = new MockAdapter(api.request);
mock
.onPost(new RegExp('aweme/v1/room/create/\?.*'))
.reply(200, loadTestData('createLiveStreamRoom.json'), {});

const roomId = '9999999999999999999';
const streamId = '9000000000000000000';
const title = 'TITLE';

const res = await api.createLiveStreamRoom(title);
const expected: CreateLiveStreamRoomResponse = {
extra: {
now: 1000000000000,
},
room: {
title,
create_time: 1000000000,
finish_time: 1000000000,
owner: {} as CommonUserDetails,
room_id: roomId,
status: 0,
stream_id: streamId,
stream_url: {
rtmp_pull_url: `http://pull-flv-l1-mus.pstatp.com/hudong/stream-${streamId}.flv`,
rtmp_push_url: `rtmp://push-rtmp-l1-mus.pstatp.com/hudong/stream-${streamId}?wsSecret=1234&wsTime=1a1a1a1a`,
sid: streamId,
},
user_count: 0,
},
status_code: 0,
};
assert.deepStrictEqual(res.data, expected);
});
});

describe('#startLiveStream()', () => {
it('a successful response should match the interface', async () => {
const api = new TikTokAPI(mockParams, mockConfig);
const mock = new MockAdapter(api.request);
mock
.onPost(new RegExp('aweme/v1/room/create/\?.*'))
.reply(200, loadTestData('createLiveStreamRoom.json'), {});
mock
.onGet(new RegExp('aweme/v1/room/update/status/\?.*'))
.reply(200, loadTestData('updateLiveStreamStatus.json'), {});

const roomId = '9999999999999999999';
const streamId = '9000000000000000000';
const title = 'TITLE';

const res = await api.startLiveStream(title);
const expected: CreateLiveStreamRoomResponse = {
extra: {
now: 1000000000000,
},
room: {
title,
create_time: 1000000000,
finish_time: 1000000000,
owner: {} as CommonUserDetails,
room_id: roomId,
status: 0,
stream_id: streamId,
stream_url: {
rtmp_pull_url: `http://pull-flv-l1-mus.pstatp.com/hudong/stream-${streamId}.flv`,
rtmp_push_url: `rtmp://push-rtmp-l1-mus.pstatp.com/hudong/stream-${streamId}?wsSecret=1234&wsTime=1a1a1a1a`,
sid: streamId,
},
user_count: 0,
},
status_code: 0,
};
assert.deepStrictEqual(res.data, expected);
});

it('should throw an error if the live stream room could not be created', async () => {
const api = new TikTokAPI(mockParams, mockConfig);
const mock = new MockAdapter(api.request);
mock
.onPost(new RegExp('aweme/v1/room/create/\?.*'))
.reply(200, { status_code: 3 }, {});

assert.isRejected(api.startLiveStream('TITLE'), /could not be created/);
});

it('should throw an error if the live stream could not be started', async () => {
const api = new TikTokAPI(mockParams, mockConfig);
const mock = new MockAdapter(api.request);
mock
.onPost(new RegExp('aweme/v1/room/create/\?.*'))
.reply(200, loadTestData('createLiveStreamRoom.json'), {});
mock
.onGet(new RegExp('aweme/v1/room/update/status/\?.*'))
.reply(200, { status_code: 3 }, {});

assert.isRejected(api.startLiveStream('TITLE'), /could not be started/);
});
});

describe('#endLiveStream()', () => {
it('a successful response should match the interface', async () => {
const api = new TikTokAPI(mockParams, mockConfig);
const mock = new MockAdapter(api.request);
mock
.onGet(new RegExp('aweme/v1/room/update/status/\?.*'))
.reply(200, loadTestData('updateLiveStreamStatus.json'), {});

const res = await api.endLiveStream('9999999999999999999', '9000000000000000000');
const expected: UpdateLiveStreamStatusResponse = {
extra: {
now: 1000000000000,
},
status_code: 0,
};
assert.deepStrictEqual(res.data, expected);
});
});

0 comments on commit edfceec

Please sign in to comment.