Skip to content

Commit

Permalink
chore: migrate WebDecoder
Browse files Browse the repository at this point in the history
  • Loading branch information
guoxianzhe committed Oct 29, 2024
1 parent db451c5 commit b76084f
Show file tree
Hide file tree
Showing 31 changed files with 1,814 additions and 494 deletions.
3 changes: 3 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@
"WebGLTexture": false,
"WebGLBuffer": false,
"WebGLProgram": false,
"VideoDecoder": false,
"VideoFrame": false,
"EncodedVideoChunk":false,
"HTMLCanvasElement": false,
"ResizeObserver": false,
"name": false,
Expand Down
1 change: 1 addition & 0 deletions example/src/main/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from 'path';
import { format as formatUrl } from 'url';

import 'agora-electron-sdk/js/Private/ipc/main.js';
import { BrowserWindow, app, ipcMain, systemPreferences } from 'electron';

const isDevelopment = process.env.NODE_ENV !== 'production';
Expand Down
264 changes: 264 additions & 0 deletions example/src/renderer/examples/basic/VideoDecoder/VideoDecoder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
import {
AgoraEnv,
ChannelProfileType,
ClientRoleType,
IRtcEngineEventHandler,
IRtcEngineEx,
LogFilterType,
RtcConnection,
RtcStats,
VideoSourceType,
createAgoraRtcEngine,
} from 'agora-electron-sdk';
import React, { ReactElement } from 'react';

import {
BaseAudioComponentState,
BaseComponent,
} from '../../../components/BaseComponent';
import { AgoraButton, AgoraTextInput } from '../../../components/ui';
import Config from '../../../config/agora.config';
import { askMediaAccess } from '../../../utils/permissions';

interface State extends BaseAudioComponentState {
token2: string;
uid2: number;
fps: number;
joinChannelExSuccess: boolean;
}

export default class VideoDecoder
extends BaseComponent<{}, State>
implements IRtcEngineEventHandler
{
protected engine?: IRtcEngineEx;

protected createState(): State {
return {
appId: Config.appId,
fps: 0,
enableVideo: true,
channelId: Config.channelId,
token: Config.token,
uid: Config.uid,
joinChannelSuccess: false,
token2: '',
uid2: 0,
remoteUsers: [],
joinChannelExSuccess: false,
};
}

/**
* Step 1: initRtcEngine
*/
protected async initRtcEngine() {
const { appId } = this.state;
if (!appId) {
this.error(`appId is invalid`);
}
this.engine = createAgoraRtcEngine() as IRtcEngineEx;
// need to enable WebCodecsDecoder before call engine.initialize
// if enableWebCodecsDecoder is true, the video stream will be decoded by WebCodecs
// will automatically register videoEncodedFrameObserver
// videoEncodedFrameObserver will be released when engine.release
AgoraEnv.enableWebCodecsDecoder = true;
this.engine.initialize({
appId,
logConfig: { filePath: Config.logFilePath },
// Should use ChannelProfileLiveBroadcasting on most of cases
channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
});
this.engine.setLogFilter(LogFilterType.LogFilterDebug);
this.engine.registerEventHandler(this);

// Need granted the microphone and camera permission
await askMediaAccess(['microphone', 'camera', 'screen']);

// Need to enable video on this case
// If you only call `enableAudio`, only relay the audio stream to the target channel
this.engine.enableVideo();
}

/**
* Step 2: joinChannel
*/
protected joinChannel() {
const { channelId, token, uid } = this.state;
if (!channelId) {
this.error('channelId is invalid');
return;
}
if (uid < 0) {
this.error('uid is invalid');
return;
}

// start joining channel
// 1. Users can only see each other after they join the
// same channel successfully using the same app id.
// 2. If app certificate is turned on at dashboard, token is needed
// when joining channel. The channel name and uid used to calculate
// the token has to match the ones used for channel join
this.engine?.joinChannel(token, channelId, uid, {
// Make myself as the broadcaster to send stream to remote
clientRoleType: ClientRoleType.ClientRoleBroadcaster,
});
}

/**
* Step 2-1(optional): joinChannelEx
*/
protected joinChannelEx = () => {
const { channelId, token2, uid2 } = this.state;
if (!channelId) {
this.error('channelId is invalid');
return;
}
if (uid2 <= 0) {
this.error('uid2 is invalid');
return;
}
// publish screen share stream
this.engine?.joinChannelEx(
token2,
{ channelId, localUid: uid2 },
{
autoSubscribeAudio: true,
autoSubscribeVideo: true,
publishMicrophoneTrack: true,
publishCameraTrack: true,
clientRoleType: ClientRoleType.ClientRoleBroadcaster,
}
);
};

/**
* Step 2-2(optional): leaveChannelEx
*/
leaveChannelEx = () => {
const { channelId, uid2 } = this.state;
this.engine?.leaveChannelEx({ channelId, localUid: uid2 });
};

/**
* Step 4: leaveChannel
*/
protected leaveChannel() {
this.engine?.leaveChannel();
}

/**
* Step 5: releaseRtcEngine
*/
protected releaseRtcEngine() {
this.engine?.unregisterEventHandler(this);
this.engine?.release();
}

onJoinChannelSuccess(connection: RtcConnection, elapsed: number) {
const { uid2 } = this.state;
if (connection.localUid === uid2) {
this.setState({ joinChannelExSuccess: true });
} else {
this.setState({ joinChannelSuccess: true });
}
this.info(
'onJoinChannelSuccess',
'connection',
connection,
'elapsed',
elapsed
);
}

onLeaveChannel(connection: RtcConnection, stats: RtcStats) {
const { uid2 } = this.state;
if (connection.localUid === uid2) {
this.setState({ joinChannelExSuccess: false });
} else {
this.setState({ joinChannelSuccess: false });
}
this.info('onLeaveChannel', 'connection', connection, 'stats', stats);
this.setState(this.createState());
}

protected renderUsers(): ReactElement | undefined {
let { remoteUsers, joinChannelSuccess, joinChannelExSuccess } = this.state;
return (
<>
{joinChannelSuccess
? remoteUsers.map((item) =>
this.renderUser({
uid: item,
// Use WebCodecs to decode video stream
useWebCodecsDecoder: true,
enableFps: true,
sourceType: VideoSourceType.VideoSourceRemote,
})
)
: undefined}
{joinChannelExSuccess
? remoteUsers.map((item) =>
this.renderUser({
uid: item,
// Use WebCodecs to decode video stream
useWebCodecsDecoder: true,
enableFps: true,
sourceType: VideoSourceType.VideoSourceRemote,
})
)
: undefined}
</>
);
}

protected renderChannel(): ReactElement | undefined {
const { channelId, joinChannelSuccess, joinChannelExSuccess } = this.state;
return (
<>
<AgoraTextInput
onChangeText={(text) => {
this.setState({ channelId: text });
}}
placeholder={`channelId`}
value={channelId}
/>
<AgoraButton
title={`${joinChannelSuccess ? 'leave' : 'join'} Channel`}
disabled={joinChannelExSuccess}
onPress={() => {
joinChannelSuccess ? this.leaveChannel() : this.joinChannel();
}}
/>
</>
);
}

protected renderConfiguration(): ReactElement | undefined {
let { joinChannelExSuccess, uid2, joinChannelSuccess } = this.state;
return (
<>
<AgoraTextInput
editable={!joinChannelExSuccess && !joinChannelSuccess}
onChangeText={(text) => {
if (isNaN(+text)) return;
this.setState({
uid2: text === '' ? this.createState().uid2 : +text,
});
}}
numberKeyboard={true}
placeholder={`uid2 (must > 0)`}
value={uid2 > 0 ? uid2.toString() : ''}
/>
<AgoraButton
title={`${joinChannelExSuccess ? 'leave' : 'join'} channelEx`}
disabled={joinChannelSuccess}
onPress={
joinChannelExSuccess ? this.leaveChannelEx : this.joinChannelEx
}
/>
</>
);
}
}
5 changes: 5 additions & 0 deletions example/src/renderer/examples/basic/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import JoinChannelAudio from './JoinChannelAudio/JoinChannelAudio';
import JoinChannelVideo from './JoinChannelVideo/JoinChannelVideo';
import StringUid from './StringUid/StringUid';
import VideoDecoder from './VideoDecoder/VideoDecoder';

const Basic = {
title: 'Basic',
Expand All @@ -17,6 +18,10 @@ const Basic = {
name: 'StringUid',
component: StringUid,
},
{
name: 'VideoDecoder',
component: VideoDecoder,
},
],
};

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
"@commitlint/config-conventional": "^17.0.2",
"@evilmartians/lefthook": "^1.2.2",
"@release-it/conventional-changelog": "^5.0.0",
"@types/dom-webcodecs": "^0.1.11",
"@types/node": "^22.8.2",
"@types/jest": "^28.1.2",
"@types/json-bigint": "^1.0.1",
"@types/lodash.isequal": "^4.5.6",
Expand Down Expand Up @@ -135,6 +137,7 @@
"jsonfile": "^6.1.0",
"lodash.isequal": "^4.5.0",
"minimist": "^1.2.5",
"semver": "^7.6.0",
"shelljs": "^0.8.4",
"ts-interface-checker": "^1.0.2",
"winston": "^3.3.3",
Expand Down
27 changes: 27 additions & 0 deletions source_code/agora_node_ext/agora_electron_bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ napi_value AgoraElectronBridge::Init(napi_env env, napi_value exports) {
napi_property_descriptor properties[] = {
DECLARE_NAPI_METHOD("CallApi", CallApi),
DECLARE_NAPI_METHOD("OnEvent", OnEvent),
DECLARE_NAPI_METHOD("UnEvent", UnEvent),
DECLARE_NAPI_METHOD("GetBuffer", GetBuffer),
DECLARE_NAPI_METHOD("EnableVideoFrameCache", EnableVideoFrameCache),
DECLARE_NAPI_METHOD("DisableVideoFrameCache", DisableVideoFrameCache),
Expand Down Expand Up @@ -253,6 +254,30 @@ napi_value AgoraElectronBridge::OnEvent(napi_env env, napi_callback_info info) {
RETURE_NAPI_OBJ();
}

napi_value AgoraElectronBridge::UnEvent(napi_env env, napi_callback_info info) {
napi_status status;
size_t argc = 2;
napi_value args[2];
napi_value jsthis;
int ret = ERR_FAILED;
status = napi_get_cb_info(env, info, &argc, args, &jsthis, nullptr);
assert(status == napi_ok);

AgoraElectronBridge *agoraElectronBridge;
status =
napi_unwrap(env, jsthis, reinterpret_cast<void **>(&agoraElectronBridge));
assert(status == napi_ok);

std::string eventName = "";
status = napi_get_value_utf8string(env, args[0], eventName);
assert(status == napi_ok);

agoraElectronBridge->_iris_rtc_event_handler->removeEvent(eventName);
ret = ERR_OK;

RETURE_NAPI_OBJ();
}

napi_value AgoraElectronBridge::SetAddonLogFile(napi_env env,
napi_callback_info info) {
napi_status status;
Expand Down Expand Up @@ -480,6 +505,8 @@ napi_value AgoraElectronBridge::InitializeEnv(napi_env env,
status =
napi_unwrap(env, jsthis, reinterpret_cast<void **>(&agoraElectronBridge));

napi_value obj0 = args[0];

agoraElectronBridge->Init();
LOG_F(INFO, __FUNCTION__);
napi_value retValue = nullptr;
Expand Down
7 changes: 7 additions & 0 deletions source_code/agora_node_ext/agora_electron_bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ namespace electron {

class NodeIrisEventHandler;

struct AgoraEnv {
bool enable_web_codecs_decoder = false;
};

class AgoraElectronBridge {
public:
explicit AgoraElectronBridge();
Expand All @@ -30,6 +34,7 @@ class AgoraElectronBridge {
static napi_value CallApi(napi_env env, napi_callback_info info);
static napi_value GetBuffer(napi_env env, napi_callback_info info);
static napi_value OnEvent(napi_env env, napi_callback_info info);
static napi_value UnEvent(napi_env env, napi_callback_info info);
static napi_value EnableVideoFrameCache(napi_env env,
napi_callback_info info);
static napi_value DisableVideoFrameCache(napi_env env,
Expand All @@ -43,8 +48,10 @@ class AgoraElectronBridge {
void OnApiError(const char *errorMessage);
void Init();
void Release();
void SetAgoraEnv(const AgoraEnv &agoraEnv) { _agoraEnv = agoraEnv; }

private:
AgoraEnv _agoraEnv;
static const char *_class_name;
static napi_ref *_ref_construcotr_ptr;
static const char *_ret_code_str;
Expand Down
Loading

0 comments on commit b76084f

Please sign in to comment.