From 1425916468b5e8a98b6c5d7fcd12aece63691bda Mon Sep 17 00:00:00 2001 From: Kishan Dhakan Date: Wed, 28 Feb 2024 14:43:11 +0530 Subject: [PATCH 1/7] copy staging --- .github/way-of-working.md | 2 +- .gitignore | 1 + .../{reference => overview}/pricing.mdx | 2 +- agora-analytics/overview/product-overview.mdx | 2 +- .../{reference => overview}/release-notes.mdx | 2 +- .../supported-platforms.mdx | 4 +- agora-chat/get-started/get-started-uikit.mdx | 2 +- .../{reference => overview}/pricing.mdx | 0 .../{reference => overview}/release-notes.mdx | 2 +- .../supported-platforms.mdx | 2 +- .../handle-expire-event.mdx | 48 + .../join-stream-channel.mdx | 97 ++ .../authentication-workflow/login.mdx | 67 ++ .../authentication-workflow/renew-token.mdx | 21 + .../retrieve-rtc-token.mdx | 41 + .../retrieve-rtm-token.mdx | 126 +++ .../engine-config/apply-encryption-config.mdx | 14 + .../engine-config/apply-geofencing-config.mdx | 9 +- .../engine-config/apply-proxy-config.mdx | 13 + .../signaling/engine-config/cloud-proxy.mdx | 26 + .../engine-config/encryption-config.mdx | 20 + .../code/signaling/engine-config/set-area.mdx | 14 + .../configure-engine-instance.mdx | 78 ++ .../get-started-sdk/declare-variables.mdx | 21 +- .../engine-instance-variable.mdx | 12 +- .../code/signaling/get-started-sdk/login.mdx | 51 + .../code/signaling/get-started-sdk/logout.mdx | 26 + .../signaling/get-started-sdk/publish.mdx | 38 + .../get-started-sdk/respond-to-events.mdx | 176 ++++ .../signaling/get-started-sdk/subscribe.mdx | 35 + .../signaling/get-started-sdk/unsubscribe.mdx | 26 + .../presence/enable-notifications.mdx | 20 + .../signaling/presence/event-listener.mdx | 100 ++ .../signaling/presence/get-user-status.mdx | 51 + assets/code/signaling/presence/list-users.mdx | 66 ++ .../signaling/presence/set-user-status.mdx | 37 + .../storage/get-channel-metadata.mdx | 59 ++ .../signaling/storage/get-user-metadata.mdx | 59 ++ .../storage/handle-metadata-events.mdx | 16 + .../code/signaling/storage/manage-locks.mdx | 250 +++++ .../storage/set-channel-metadata.mdx | 84 +- .../signaling/storage/set-user-metadata.mdx | 66 +- .../storage/subscribe-user-metadata.mdx | 35 + .../storage/update-user-metadata.mdx | 66 +- .../stream-channel/join-leave-channel.mdx | 97 +- .../stream-channel/join-leave-topic.mdx | 92 ++ .../stream-channel/publish-message.mdx | 46 +- .../stream-channel/subscribe-topic.mdx | 46 + .../ai-noise-suppression/configure-engine.mdx | 18 + .../configure-extension.mdx | 41 + .../ai-noise-suppression/enable-denoiser.mdx | 74 ++ .../ai-noise-suppression/import-library.mdx | 23 + .../ai-noise-suppression/import-plugin.mdx | 25 + .../set-noise-reduction-mode.mdx | 34 + .../set-reduction-level.mdx | 34 + .../ai-noise-suppression/setup-logging.mdx | 21 + .../apply-voice-effects.mdx | 129 +++ .../{swift => }/configure-buttons.mdx | 4 +- .../audio-voice-effects/configure-engine.mdx | 69 ++ .../audio-voice-effects/create-ui.mdx | 51 + .../audio-voice-effects/event-handler.mdx | 98 ++ .../audio-voice-effects/import-library.mdx | 26 + .../audio-voice-effects/pause-play-resume.mdx | 105 ++ .../audio-voice-effects/preload-effect.mdx | 47 + .../audio-voice-effects/set-audio-profile.mdx | 41 + .../audio-voice-effects/set-audio-route.mdx | 87 ++ .../audio-voice-effects/set-variables.mdx | 29 + .../audio-voice-effects/stop-start-mixing.mdx | 145 +++ .../swift/apply-voice-effects.mdx | 86 -- .../audio-voice-effects/swift/create-ui.mdx | 15 - .../swift/pause-play-resume.mdx | 56 -- .../swift/set-audio-route.mdx | 18 - .../swift/stop-start-mixing.mdx | 40 - .../{swift => }/update-ui.mdx | 0 .../authentication-workflow/add-variables.mdx | 44 + .../authentication-workflow/event-handler.mdx | 173 ++++ .../authentication-workflow/fetch-token.mdx | 182 ++++ .../import-library.mdx | 38 + .../authentication-workflow/join-channel.mdx | 220 +++++ .../authentication-workflow/renew-token.mdx | 48 + .../{swift => }/specify-channel.mdx | 4 +- .../swift/add-variables.mdx | 42 - .../swift/fetch-token.mdx | 18 - .../swift/join-channel.mdx | 75 -- .../cloud-proxy/configure-engine.mdx | 18 + .../cloud-proxy/connection-failed.mdx | 63 ++ .../video-sdk/cloud-proxy/event-handler.mdx | 103 ++ .../video-sdk/cloud-proxy/import-library.mdx | 28 + .../video-sdk/cloud-proxy/set-cloud-proxy.mdx | 70 ++ .../video-sdk/cloud-proxy/set-variables.mdx | 27 + .../configure-engine-audio.mdx | 20 + .../configure-engine.mdx | 13 + .../create-custom-audio-track.mdx | 37 + .../create-custom-video-track.mdx | 33 + .../destroy-custom-track-audio.mdx | 26 + .../destroy-custom-track-video.mdx | 16 + .../enable-audio-publishing.mdx | 103 ++ .../enable-video-publishing.mdx | 105 ++ .../import-library-audio.mdx | 24 + .../custom-video-and-audio/import-library.mdx | 37 + .../push-audio-frames.mdx | 115 +++ .../push-video-frames.mdx | 88 ++ .../read-audio-input.mdx | 71 ++ .../render-custom-video.mdx | 98 ++ .../set-variables-audio.mdx | 40 + .../custom-video-and-audio/set-variables.mdx | 39 + .../enable-encryption.mdx | 149 +++ .../enable-end-to-end-encryption.mdx | 174 ++++ .../encrypt-media-streams/event-handler.mdx | 56 ++ .../encrypt-media-streams/import-library.mdx | 25 + .../encrypt-media-streams/set-variables.mdx | 18 + .../ensure-channel-quality/event-handler.mdx | 404 ++++++++ .../implement-call-quality-view.mdx | 52 + .../implement-declarations.mdx | 121 +++ .../{swift => }/implement-labels.mdx | 0 .../{swift => }/implement-network-status.mdx | 21 +- .../ensure-channel-quality/import-library.mdx | 57 ++ .../ensure-channel-quality/probe-test.mdx | 125 +++ .../set-audio-video-profile.mdx | 28 + .../ensure-channel-quality/set-latency.mdx | 65 ++ .../ensure-channel-quality/setup-engine.mdx | 367 +++++++ .../ensure-channel-quality/show-stats.mdx | 45 + .../swift/implement-call-quality-view.mdx | 25 - .../swift/implement-declarations.mdx | 13 - .../ensure-channel-quality/switch-quality.mdx | 127 +++ .../ensure-channel-quality/test-hardware.mdx | 407 ++++++++ .../video-sdk/geofencing/combine-geofence.mdx | 34 + .../video-sdk/geofencing/set-geofence.mdx | 94 ++ .../get-started-sdk/create-engine.mdx | 286 ++++++ .../get-started-sdk/declare-variables.mdx | 140 +++ .../video-sdk/get-started-sdk/destroy.mdx | 67 ++ .../get-started-sdk/handle-events.mdx | 407 ++++++++ .../get-started-sdk/import-library.mdx | 76 ++ .../get-started-sdk/join-channel.mdx | 383 ++++++++ .../get-started-sdk/leave-channel.mdx | 145 +++ .../video-sdk/get-started-sdk/local-video.mdx | 123 +++ .../get-started-sdk/remote-video.mdx | 153 +++ .../get-started-sdk/request-permissions.mdx | 102 ++ .../get-started-sdk/set-user-role.mdx | 88 ++ .../setup-audio-video-tracks.mdx | 9 + .../get-started-sdk/swift/create-ui.mdx | 278 ------ .../get-started-sdk/swift/handle-events.mdx | 17 - .../get-started-sdk/swift/join-and-leave.mdx | 239 ----- .../get-started-sdk/swift/join-channel.mdx | 39 - .../get-started-sdk/swift/leave-channel.mdx | 13 - .../get-started-sdk/swift/role-action.mdx | 14 - .../get-started-sdk/swift/show-message.mdx | 26 - .../swift/view-did-disappear.mdx | 19 - .../import-library.mdx | 43 + .../join-a-second-channel.mdx | 236 +++++ .../leave-second-channel.mdx | 35 + .../monitor-channel-media-relay-state.mdx | 111 +++ .../receive-callbacks-from-second-channel.mdx | 94 ++ .../set-variables.mdx | 72 ++ .../start-stop-channel-media-relay.mdx | 272 ++++++ .../swift/configure-buttons.mdx | 4 +- .../swift/create-ui.mdx | 4 +- .../swift/mc-configure-buttons.mdx | 4 +- .../swift/mc-create-ui.mdx | 4 +- .../swift/mc-join-second-channel.mdx | 4 +- .../swift/mc-second-channel-delegate.mdx | 4 +- .../swift/monitor-relay-state.mdx | 4 +- .../video-sdk/play-media/configure-engine.mdx | 21 + .../play-media/destroy-media-player.mdx | 52 + .../video-sdk/play-media/display-media.mdx | 68 ++ .../video-sdk/play-media/event-handler.mdx | 191 ++++ .../video-sdk/play-media/import-library.mdx | 24 + .../play-media/play-pause-resume.mdx | 110 +++ .../video-sdk/play-media/set-variables.mdx | 39 + .../video-sdk/play-media/start-streaming.mdx | 110 +++ .../play-media/swift/configure-buttons.mdx | 4 +- .../video-sdk/play-media/swift/create-ui.mdx | 4 +- .../swift/open-play-pause-media.mdx | 4 +- .../update-channel-publish-options.mdx | 79 ++ .../product-workflow/import-library.mdx | 52 + .../product-workflow/ios-extension.mdx | 80 ++ .../product-workflow/macos-screencapture.mdx | 88 ++ .../product-workflow/media-device-changed.mdx | 86 ++ .../microphone-camera-change.mdx | 79 ++ .../product-workflow/mute-local-video.mdx | 21 + .../product-workflow/mute-remote-user.mdx | 77 ++ .../override-broadcast-started.mdx | 25 + .../product-workflow/preview-screen-track.mdx | 40 + .../product-workflow/publish-screen-track.mdx | 40 + .../product-workflow/screen-sharer-target.mdx | 23 + .../product-workflow/setup-engine.mdx | 20 + .../product-workflow/setup-volume.mdx | 153 +++ .../product-workflow/start-sharing.mdx | 167 ++++ .../product-workflow/stop-sharing.mdx | 41 + .../raw-video-audio/configure-engine.mdx | 77 ++ .../raw-video-audio/import-library.mdx | 31 + .../raw-video-audio/modify-audio-video.mdx | 177 ++++ .../register-video-audio-frame-observers.mdx | 106 ++ .../set-audio-frame-observer.mdx | 168 ++++ .../raw-video-audio/set-variables.mdx | 101 ++ .../set-video-frame-observer.mdx | 104 ++ .../swift/register-frame-observers.mdx | 4 +- .../swift/unregister-frame-observers.mdx | 4 +- ...unregister-video-audio-frame-observers.mdx | 57 ++ .../spatial-audio/import-library.mdx | 35 + .../video-sdk/spatial-audio/play-media.mdx | 49 + .../spatial-audio/remove-spatial.mdx | 37 + .../video-sdk/spatial-audio/set-variables.mdx | 58 ++ .../video-sdk/spatial-audio/setup-local.mdx | 98 ++ .../video-sdk/spatial-audio/setup-remote.mdx | 147 +++ .../video-sdk/spatial-audio/setup-spatial.mdx | 151 +++ .../virtual-background/blur-background.mdx | 75 ++ .../virtual-background/color-background.mdx | 101 ++ .../virtual-background/configure-engine.mdx | 26 + .../device-compatibility.mdx | 48 + .../virtual-background/image-background.mdx | 66 ++ .../virtual-background/import-library.mdx | 61 ++ .../virtual-background/reset-background.mdx | 60 ++ .../set-virtual-background.mdx | 115 +++ .../get-started-sdk/swift/create-ui.mdx | 4 +- .../get-started-sdk/swift/show-message.mdx | 4 +- .../swift/view-did-disappear.mdx | 4 +- .../images/chat/chat-call-logic-android.svg | 2 +- .../images/chat/chat-call-logic-flutter.svg | 2 +- assets/images/chat/chat-call-logic-unity.svg | 2 +- .../images/chat/chat-call-logic-windows.svg | 2 +- .../active-fence-add-an-editable-field.png | Bin 0 -> 88758 bytes .../extensions-marketplace/active-fence.puml | 2 +- .../extensions-marketplace/active-fence.svg | 2 +- .../extensions-marketplace/geofencing.svg | 1 + .../livedata-ios-build-settings-add-objc.png | Bin 0 -> 165224 bytes ...-ios-build-settings-capitalize-o-and-c.png | Bin 0 -> 351127 bytes .../extensions-marketplace/ncs-worflow.svg | 1 + .../images/flexible-classroom/ios-demo-qr.png | Bin 0 -> 2958 bytes .../ils-call-logic-android.svg | 491 +--------- .../ils-call-logic-flutter.svg | 489 +--------- .../ils-call-logic-ios.svg | 485 +--------- .../ils-call-logic-template.svg | 485 +--------- .../ils-call-logic-unity.svg | 495 +--------- .../ils-call-logic-web.svg | 485 +--------- .../live-streaming-over-multiple-channels.svg | 1 + assets/images/iot/iot-channel-quality.svg | 2 +- assets/images/iot/iot-get-started.svg | 2 +- assets/images/iot/iot-licensing.svg | 2 +- assets/images/iot/iot-multi-channel.svg | 2 +- .../media-gateway/media-gateway-flow.svg | 1 - .../media-gateway/obs-server-setting.png | Bin 17561 -> 0 bytes .../media-gateway/obs-setting-bframes.png | Bin 43084 -> 0 bytes .../media-gateway/obs-setting-zerolatency.png | Bin 42522 -> 0 bytes .../media-gateway/overview-page-image.png | Bin 32914 -> 0 bytes .../ncs-cloud-recording-workflow.svg | 2 +- .../ncs-media-pull.svg | 2 +- .../ncs-media-push.svg | 2 +- assets/images/others/authentication-logic.svg | 451 +-------- .../others/documentation_way_of_working.svg | 511 +--------- .../images/others/media-stream-encryption.svg | 463 +-------- assets/images/others/play-media.svg | 469 +-------- .../real-time-transcription.png | Bin 3828 -> 0 bytes .../transcriptions.svg | 7 - assets/images/shared/ncs-worflow.svg | 1 + .../signaling/authentication-workflow.puml | 3 +- .../signaling/authentication-workflow.svg | 2 +- assets/images/signaling/cloud-proxy.svg | 2 +- assets/images/signaling/configure_project.png | Bin 0 -> 47633 bytes assets/images/signaling/data-encryption.puml | 2 +- assets/images/signaling/data-encryption.svg | 2 +- .../signaling/get-started-workflow.puml | 3 +- .../images/signaling/get-started-workflow.svg | 2 +- .../images/signaling/presence-workflow.puml | 3 +- assets/images/signaling/presence-workflow.svg | 2 +- .../signaling/project_settings_signaling.png | Bin 0 -> 108708 bytes .../signaling-metadata-workflow.puml | 3 +- .../signaling/signaling-metadata-workflow.svg | 2 +- .../signaling/signaling-pricing-plans.png | Bin 0 -> 55371 bytes .../signaling/stream-channel-workflow.puml | 3 +- .../signaling/stream-channel-workflow.svg | 2 +- assets/images/video-calling/geofencing.svg | 445 +-------- .../video-calling/process-raw-video-audio.svg | 479 +-------- .../video-call-logic-android.svg | 471 +-------- .../video-call-logic-flutter.svg | 465 +-------- .../video-calling/video-call-logic-ios.svg | 469 +-------- .../video-call-logic-template.svg | 469 +-------- .../video-calling/video-call-logic-unity.svg | 473 +-------- .../video-calling/video-call-logic-web.svg | 465 +-------- .../video-calling/video-composite-example.png | Bin 9449 -> 0 bytes .../images/video-calling/video-compositor.png | Bin 63427 -> 0 bytes .../video-calling/video_call_workflow.svg | 1 + .../video_call_workflow_run_end.svg | 1 + assets/images/video-sdk/agora_skin.iuml | 212 ++-- .../audio-and-voice-effects-web.puml | 41 + .../video-sdk/audio-and-voice-effects-web.svg | 1 + .../video-sdk/audio-and-voice-effects.svg | 497 +--------- .../images/video-sdk/authentication-logic.svg | 451 +-------- assets/images/video-sdk/cloud-proxy.svg | 2 +- .../video-sdk/custom-source-video-audio.svg | 471 +-------- .../video-sdk/ensure-channel-quality.svg | 481 +--------- .../video-sdk/ils-call-logic-android.svg | 491 +--------- .../video-sdk/ils-call-logic-electron.svg | 483 +--------- .../video-sdk/ils-call-logic-flutter.svg | 487 +--------- .../images/video-sdk/ils-call-logic-ios.svg | 485 +--------- .../video-sdk/ils-call-logic-template.svg | 485 +--------- .../video-sdk/ils-call-logic-unity.puml | 12 +- .../images/video-sdk/ils-call-logic-unity.svg | 493 +--------- .../video-sdk/ils-call-logic-unreal.puml | 2 +- .../video-sdk/ils-call-logic-unreal.svg | 2 +- .../images/video-sdk/ils-call-logic-web.svg | 485 +--------- .../video-sdk/integrated-token-generation.svg | 449 +-------- .../video-sdk/media-stream-encryption.svg | 457 +-------- assets/images/video-sdk/play-drm-music.svg | 463 +-------- .../images/video-sdk/product-workflow-web.svg | 483 +--------- assets/images/video-sdk/product-workflow.svg | 1 + .../real-time-transcription-server.puml | 51 - .../real-time-transcription-server.svg | 1 - assets/images/video-sdk/spatial-audio-web.svg | 2 +- assets/images/video-sdk/spatial-audio.svg | 479 +-------- .../video-sdk/video-call-logic-android.puml | 3 +- .../video-sdk/video-call-logic-android.svg | 2 +- .../video-sdk/video-call-logic-electron.svg | 2 +- .../video-sdk/video-call-logic-flutter.svg | 2 +- .../images/video-sdk/video-call-logic-ios.svg | 2 +- .../video-sdk/video-call-logic-reactjs.puml | 13 +- .../video-sdk/video-call-logic-reactjs.svg | 2 +- .../video-sdk/video-call-logic-template.svg | 469 +-------- .../video-sdk/video-call-logic-unity.puml | 1 - .../video-sdk/video-call-logic-unity.svg | 2 +- .../video-sdk/video-call-logic-unreal.puml | 2 +- .../video-sdk/video-call-logic-unreal.svg | 2 +- .../images/video-sdk/video-call-logic-web.svg | 2 +- .../images/video-sdk/video_call_workflow.svg | 1 + .../video-sdk/video_call_workflow_run_end.svg | 1 + .../images/voice-sdk/authentication-logic.svg | 451 +-------- .../images/voice-sdk/ensure-voice-quality.svg | 495 +--------- assets/images/voice-sdk/geofencing.svg | 445 +-------- .../voice-sdk/integrated-token-generation.svg | 449 +-------- assets/images/voice-sdk/process-raw-audio.svg | 1 + .../voice-sdk/product-workflow-voice-web.svg | 477 +-------- .../voice-sdk/product-workflow-voice.svg | 1 + .../voice-sdk/voice-call-logic-android.puml | 33 + .../voice-sdk/voice-call-logic-android.svg | 463 +-------- .../voice-sdk/voice-call-logic-electron.svg | 455 +-------- .../voice-sdk/voice-call-logic-flutter.svg | 455 +-------- .../voice-sdk/voice-call-logic-unity.puml | 5 +- .../voice-sdk/voice-call-logic-unity.svg | 471 +-------- .../develop/media-stream-encryption.mdx | 2 +- .../enable-features/image-enhancement.mdx | 13 - .../{reference => overview}/pricing.mdx | 2 +- .../{reference => overview}/release-notes.mdx | 2 +- .../supported-platforms.mdx | 0 broadcast-streaming/reference/error-codes.mdx | 13 + .../reference/known-issues.mdx | 15 - .../reference/service-limits.mdx | 13 + cloud-recording/develop/individual-mode.md | 14 +- .../develop/integration-best-practices.md | 46 +- .../develop/recording-video-profile.md | 2 +- cloud-recording/develop/screen-capture.md | 4 +- .../develop/webpage-best-practices.md | 39 + cloud-recording/develop/webpage-mode.md | 2 +- cloud-recording/get-started/getstarted.md | 4 +- .../pricing-webpage-recording.md | 6 +- .../{reference => overview}/pricing.md | 13 +- cloud-recording/overview/product-overview.mdx | 2 +- .../{reference => overview}/release-notes.mdx | 2 +- .../supported-platforms.mdx | 2 +- cloud-recording/reference/rest-api/start.md | 2 +- .../develop/integrate/banuba.mdx | 4 +- .../develop/integrate/byteplus.mdx | 2 +- .../livedata-conversation-intelligence.mdx | 12 + .../develop/integrate/superclarity.mdx | 13 - .../develop/integrate/symbl_ai.mdx | 4 +- .../develop/integrate/synervoz.mdx | 2 +- .../{reference => overview}/release-notes.mdx | 2 +- .../supported-platforms.mdx | 2 +- .../client-api}/_category_.json | 2 +- .../classroom-sdk.mdx | 0 .../edu-context-sdk.mdx | 0 .../{reference => client-api}/proctor-sdk.mdx | 0 .../{reference => client-api}/ui-scene.mdx | 2 +- .../develop/authentication-workflow.mdx | 32 +- .../develop/classroom-security.md | 4 +- .../develop/proctor-exams-online.mdx | 8 +- flexible-classroom/develop/record-a-class.mdx | 18 +- .../develop/supply-course-materials.md | 182 +++- .../get-started/enable-flexible-classroom.mdx | 16 +- .../get-started/get-started-uibuilder.mdx | 2 +- .../get-started/get-started.mdx | 4 +- .../manage-agora-account.mdx | 2 +- flexible-classroom/overview/core-concepts.mdx | 2 +- .../{reference => overview}/downloads.mdx | 11 +- .../{reference => overview}/pricing.md | 10 +- .../overview/product-features.mdx | 14 +- .../overview/product-overview.mdx | 4 +- .../{reference => overview}/release-notes.mdx | 2 +- .../supported-platforms.md | 0 .../technical-architecture.md | 0 flexible-classroom/reference/_category_.json | 2 +- .../restful-api/_category_.json | 6 + .../classroom-api.mdx | 263 +++-- .../develop/media-stream-encryption.mdx | 2 +- .../{reference => overview}/pricing.mdx | 2 +- .../{reference => overview}/release-notes.mdx | 2 +- .../supported-platforms.mdx | 0 .../reference/error-codes.mdx | 13 + .../reference/known-issues.mdx | 15 - .../reference/service-limits.mdx | 13 + .../develop/authentication-workflow.md | 4 +- .../develop/enable-whiteboard.md | 10 +- .../develop/file-conversion-overview.md | 2 +- .../develop/migration-guide.md | 6 +- .../develop/whiteboard-tools.mdx | 4 - .../{reference => overview}/pricing.md | 7 +- .../release-notes-uikit.mdx | 2 +- .../{reference => overview}/release-notes.mdx | 2 +- .../supported-platforms.mdx | 2 +- .../reference/downloads.mdx | 9 +- .../file-conversion-overview-deprecated.mdx | 2 +- .../reference/uikit-sdk.mdx | 2 - .../file-conversion-deprecated.mdx | 2 +- .../whiteboard-api/file-conversion.md | 2 +- iot/develop/media-stream-encryption.mdx | 2 +- iot/{reference => overview}/pricing.mdx | 2 +- iot/{reference => overview}/release-notes.mdx | 2 +- .../supported-platforms.mdx | 0 media-gateway/develop/_category_.json | 6 - media-gateway/get-started/_category_.json | 6 - media-gateway/get-started/quickstart.mdx | 13 - media-gateway/overview/_category_.json | 6 - media-gateway/overview/core-concepts.mdx | 14 - media-gateway/overview/product-overview.mdx | 54 -- media-gateway/reference/_category_.json | 6 - media-gateway/reference/best-practice.mdx | 13 - media-gateway/reference/glossary.mdx | 14 - .../reference/manage-agora-account.mdx | 14 - media-gateway/reference/release-notes.mdx | 14 - media-gateway/reference/security.mdx | 14 - .../develop/integration-best-practices.mdx | 49 +- .../{reference => overview}/pricing.mdx | 2 +- media-pull/overview/product-overview.mdx | 2 +- .../{reference => overview}/release-notes.mdx | 0 .../develop/integration-best-practices.mdx | 71 +- .../{reference => overview}/pricing.mdx | 2 +- media-push/overview/product-overview.mdx | 2 +- .../{reference => overview}/billing.md | 4 +- .../{reference => overview}/release-notes.mdx | 4 +- on-premise-recording/reference/sunset.md | 2 +- .../reference/video-profile.md | 2 +- .../get-started/_category_.json | 6 - .../get-started/get-started.mdx | 14 - .../overview/_category_.json | 6 - .../overview/core-concepts.mdx | 15 - .../overview/product-overview.mdx | 55 -- .../reference/glossary.mdx | 14 - .../reference/manage-agora-account.mdx | 14 - real-time-transcription/reference/pricing.mdx | 41 - .../reference/security.mdx | 14 - .../develop/media-stream-encryption.mdx | 2 +- .../{reference => overview}/pricing.mdx | 2 +- server-gateway/overview/product-overview.mdx | 8 +- .../{reference => overview}/release-notes.mdx | 2 +- shared/agora-analytics/_alarm.mdx | 10 +- shared/agora-analytics/_api.mdx | 10 +- shared/agora-analytics/_call-search.mdx | 8 +- shared/agora-analytics/_data-insight-plus.mdx | 2 +- shared/agora-analytics/_data-insight.mdx | 4 +- shared/agora-analytics/_embedded.mdx | 8 +- shared/agora-analytics/_monitor.mdx | 4 +- shared/agora-analytics/_pricing.mdx | 4 +- .../get-started/_agora-domain.mdx | 4 +- .../_enable-broadcast-streaming.mdx | 2 +- .../reference/_message-notification.mdx | 4 +- .../reference/_message-notification.mdx | 10 +- shared/chat-sdk/client-api/_presence.mdx | 2 +- shared/chat-sdk/client-api/_reaction.mdx | 2 +- .../project-implementation/web.mdx | 6 +- .../project-implementation/android.mdx | 20 +- .../project-implementation/flutter.mdx | 11 +- .../project-implementation/ios.mdx | 11 +- .../project-implementation/react-native.mdx | 9 +- .../project-implementation/unity.mdx | 7 +- .../project-implementation/web.mdx | 17 +- .../project-implementation/windows.mdx | 7 +- .../chat-room/_manage-chatroom-members.mdx | 2 - .../chat-room/_manage-chatrooms.mdx | 2 +- .../project-implementation/android.mdx | 9 +- .../project-implementation/flutter.mdx | 7 +- .../project-implementation/ios.mdx | 7 +- .../project-implementation/react-native.mdx | 127 ++- .../project-implementation/unity.mdx | 7 +- .../project-implementation/web.mdx | 6 +- .../project-implementation/windows.mdx | 22 +- .../messages/_translate-messages.mdx | 4 +- .../project-implementation/android.mdx | 30 +- .../project-implementation/ios.mdx | 30 + .../project-implementation/react-native.mdx | 34 + .../project-implementation/windows.mdx | 34 + .../manage-messages/understand/android.mdx | 3 + .../manage-messages/understand/flutter.mdx | 3 + .../manage-messages/understand/ios.mdx | 3 + .../understand/react-native.mdx | 6 +- .../manage-messages/understand/unity.mdx | 2 + .../manage-messages/understand/windows.mdx | 2 + .../project-implementation/android.mdx | 8 +- .../project-implementation/flutter.mdx | 7 +- .../project-implementation/ios.mdx | 7 +- .../project-implementation/react-native.mdx | 7 +- .../project-implementation/unity.mdx | 8 +- .../project-implementation/web.mdx | 6 +- .../project-implementation/windows.mdx | 7 +- .../project-implementation/ios.mdx | 2 +- .../project-implementation/android.mdx | 8 +- .../project-implementation/flutter.mdx | 8 +- .../reaction/project-implementation/ios.mdx | 8 +- .../project-implementation/react-native.mdx | 10 +- .../reaction/project-implementation/unity.mdx | 10 +- .../reaction/project-implementation/web.mdx | 8 +- .../project-implementation/windows.mdx | 9 +- .../threading/_thread-management.mdx | 2 +- .../client-api/threading/_thread-messages.mdx | 2 +- .../project-implementation/flutter.mdx | 16 +- shared/chat-sdk/develop/_authentication.mdx | 14 +- .../chat-sdk/develop/_content-moderation.mdx | 6 +- shared/chat-sdk/develop/_setup-webhooks.mdx | 2 +- .../project-implementation/android.mdx | 2 +- .../project-implementation/flutter.mdx | 2 +- .../project-implementation/ios.mdx | 2 +- .../project-implementation/react-native.mdx | 2 +- .../project-implementation/web.mdx | 14 +- .../offline-push/project-setup/android.mdx | 8 +- .../offline-push/project-setup/flutter.mdx | 8 +- .../offline-push/project-setup/ios.mdx | 10 +- .../project-setup/react-native.mdx | 11 +- .../develop/offline-push/whats-next/ios.mdx | 8 +- shared/chat-sdk/get-started/_enable.mdx | 14 +- .../get-started/_get-started-uikit.mdx | 38 +- .../get-started-sdk/project-setup/android.mdx | 9 + .../get-started-sdk/project-setup/flutter.mdx | 21 +- .../prerequisites/flutter.mdx | 16 + .../get-started-uikit/prerequisites/index.mdx | 4 + .../get-started-uikit/prerequisites/ios.mdx | 2 +- .../prerequisites/react-native.mdx | 43 + .../get-started-uikit/prerequisites/web.mdx | 2 +- .../project-implementation/flutter.mdx | 410 ++++++++ .../project-implementation/index.mdx | 5 +- .../project-implementation/react-native.mdx | 47 + .../project-setup/android.mdx | 75 +- .../project-setup/flutter.mdx | 55 ++ .../get-started-uikit/project-setup/index.mdx | 4 +- .../project-setup/react-native.mdx | 115 ++- .../project-test/flutter.mdx | 21 + .../get-started-uikit/project-test/index.mdx | 2 + .../project-test/react-native.mdx | 53 + .../get-started-uikit/reference/flutter.mdx | 315 ++++++ .../get-started-uikit/reference/index.mdx | 2 + .../reference/react-native.mdx | 22 +- shared/chat-sdk/hide/_token-server-new.mdx | 2 +- .../chat-sdk/overview/_product-overview.mdx | 2 +- shared/chat-sdk/reference/_access-token-2.mdx | 4 +- .../chat-sdk/reference/_callbacks-events.mdx | 16 +- .../reference/_chat_receive_webhook.mdx | 98 +- .../chat-sdk/reference/_chatroom-overview.mdx | 6 +- shared/chat-sdk/reference/_group-overview.mdx | 17 +- .../chat-sdk/reference/_http-status-codes.mdx | 4 +- shared/chat-sdk/reference/_ip_whitelist.mdx | 4 +- shared/chat-sdk/reference/_limitations.mdx | 4 + .../chat-sdk/reference/_message-overview.mdx | 4 +- .../reference/_pricing-plan-details.mdx | 2 +- shared/chat-sdk/reference/_pricing.mdx | 6 +- .../reference/_security-best-practice.mdx | 2 +- .../reference/error-codes/android.mdx | 8 +- shared/chat-sdk/reference/error-codes/ios.mdx | 8 +- shared/chat-sdk/reference/error-codes/web.mdx | 6 +- .../reference/error-codes/windows.mdx | 8 +- .../chat-sdk/reference/release-notes/ios.mdx | 2 +- .../reference/release-notes/react-native.mdx | 11 +- .../reference/release-notes/unity.mdx | 18 + .../chat-sdk/reference/release-notes/web.mdx | 2 +- .../restful-api/_message-management.mdx | 112 ++- .../_offline-push-configuration.mdx | 2 +- shared/chat-sdk/restful-api/_presence.mdx | 2 +- .../_push-notification-management.mdx | 2 +- .../_user-attributes-management.mdx | 6 +- .../_create-delete-retrieve-groups.mdx | 16 +- .../_manage-group-announcement-files.mdx | 37 +- .../_manage-group-members.mdx | 2 +- .../_manage-group-mutelist.mdx | 11 +- .../_manage-chatroom-attributes.mdx | 4 +- .../chatroom-management/_manage-chatrooms.mdx | 3 +- .../restful-api/shared/common-parameters.mdx | 2 +- .../_create-delete-retrieve-threads.mdx | 4 +- .../_manage-thread-members.mdx | 2 +- shared/cloud-gateway/develop/_cloud-proxy.mdx | 18 +- .../develop/_media-stream-encryption.mdx | 4 +- shared/cloud-gateway/reference/_pricing.mdx | 2 +- .../reference/release-notes/cpp.mdx | 43 +- .../reference/release-notes/java.mdx | 15 +- shared/common/_channel-management-api.mdx | 14 +- shared/common/_core-concepts.mdx | 4 +- shared/common/_restful-authentication.mdx | 2 +- .../_agora-console-restapi.mdx | 26 +- .../manage-agora-account/_check-usage.mdx | 2 +- .../_console-overview.mdx | 2 +- .../manage-agora-account/_delete-account.mdx | 8 +- .../manage-agora-account/_get-appid-token.mdx | 13 +- .../manage-agora-account/_manage-member.mdx | 8 +- .../manage-agora-account/_manage-profile.mdx | 2 +- .../manage-agora-account/_manage-projects.mdx | 2 +- .../manage-agora-account/_online-payment.mdx | 8 +- .../_sign-in-and-sign-up.mdx | 4 +- .../common/manage-agora-account/_ticket.mdx | 2 +- shared/common/no-uikit-wo-wrappers.mdx | 2 +- shared/common/no-uikit.mdx | 7 +- shared/common/prerequities-get-started.mdx | 2 + shared/common/prerequities.mdx | 27 +- shared/common/project-setup/android.mdx | 25 +- shared/common/project-setup/electron.mdx | 26 +- shared/common/project-setup/flutter.mdx | 15 +- shared/common/project-setup/index.mdx | 22 +- shared/common/project-setup/ios.mdx | 30 +- shared/common/project-setup/linux-cpp.mdx | 22 + shared/common/project-setup/macos.mdx | 34 +- .../project-setup/new-project/android.mdx | 70 ++ .../project-setup/new-project/blueprint.mdx | 17 + .../project-setup/new-project/electron.mdx | 22 + .../project-setup/new-project}/flutter.mdx | 0 .../project-setup/new-project/index.mdx | 24 + .../project-setup/new-project}/ios.mdx | 2 +- .../project-setup/new-project}/macos.mdx | 0 .../project-setup/new-project/react-js.mdx | 26 + .../new-project/react-native.mdx | 61 ++ .../project-setup/new-project/swift.mdx | 55 ++ .../project-setup/new-project/unity.mdx | 21 + .../project-setup/new-project/unreal.mdx | 29 + .../common/project-setup/new-project/web.mdx | 37 + .../project-setup/new-project/windows.mdx | 59 ++ shared/common/project-setup/react-js.mdx | 15 +- shared/common/project-setup/react-native.mdx | 68 +- shared/common/project-setup/unity.mdx | 49 +- shared/common/project-setup/web.mdx | 30 +- shared/common/project-setup/windows.mdx | 71 +- shared/common/project-test/android.mdx | 40 +- shared/common/project-test/clone-project.mdx | 31 + shared/common/project-test/electron.mdx | 4 +- .../project-test/generate-temp-rtc-token.mdx | 1 + shared/common/project-test/index.mdx | 6 + shared/common/project-test/load-web-demo.mdx | 1 + shared/common/project-test/macos.mdx | 1 - .../common/project-test/open-config-file.mdx | 36 + shared/common/project-test/react-js.mdx | 16 +- shared/common/project-test/react-native.mdx | 72 +- .../common/project-test/rtc-first-steps.mdx | 26 + .../common/project-test/run-reference-app.mdx | 57 +- shared/common/project-test/set-app-id.mdx | 1 + .../project-test/set-authentication-rtc.mdx | 11 + shared/common/project-test/swift.mdx | 18 +- shared/common/project-test/windows.mdx | 7 +- shared/common/security/_security-practice.mdx | 4 +- .../_develop-an-audio-filter.mdx | 4 +- .../extensions-marketplace/_superclarity.mdx | 118 --- .../_use-an-extension.mdx | 11 +- .../activefence/index.mdx | 44 +- .../ai-noise-suppression.mdx | 34 +- .../project-implementation/index.mdx | 12 +- .../project-implementation/poc3.mdx | 45 + .../project-implementation/web.mdx | 2 +- .../project-test/poc3.mdx | 22 + .../project-test/react-js.mdx | 23 + .../ai-noise-suppression/reference/index.mdx | 1 + .../ai-noise-suppression/reference/ios.mdx | 2 - .../ai-noise-suppression/reference/macos.mdx | 2 - .../ai-noise-suppression/reference/web.mdx | 5 + .../common/_prerequities.mdx | 2 +- .../common/project-test/poc3.mdx | 14 + .../project-implementation/cpp.mdx | 4 +- .../develop-an-audio-filter/_web.mdx | 2 +- .../project-implementation/cpp.mdx | 4 +- .../drm-play/project-implementation/swift.mdx | 8 +- .../dubbing-voice-changer/index.mdx | 2 +- .../project-implementation/android.mdx | 2 +- .../project-implementation/ios.mdx | 2 +- .../reference/android.mdx | 2 +- .../dubbing-voice-changer/reference/ios.mdx | 4 +- .../faceunity/project-implementation/ios.mdx | 2 +- .../image-enhancement.mdx | 12 +- .../integrations/bose-pinpoint.mdx | 160 ---- .../index.mdx | 79 ++ .../project-implementation/android.mdx | 61 ++ .../project-implementation}/index.mdx | 3 - .../project-implementation/ios.mdx | 82 ++ .../project-setup/android.mdx | 22 + .../project-setup/index.mdx | 5 + .../project-setup/ios.mdx | 26 + .../reference/android.mdx | 9 + .../reference/index.mdx | 5 + .../reference/ios.mdx | 8 + .../reference/_ains.mdx | 2 +- .../project-implementation/android.mdx | 4 +- .../project-implementation/electron.mdx | 2 +- .../project-implementation/flutter.mdx | 6 +- .../project-implementation/react-native.mdx | 2 +- .../project-implementation/swift.mdx | 2 +- .../project-implementation/unity.mdx | 6 +- .../project-implementation/web.mdx | 2 +- .../project-setup/android.mdx | 2 +- .../use-an-extension/project-setup/web.mdx | 2 +- .../virtual-background.mdx | 10 +- .../project-implementation/index.mdx | 14 +- .../project-implementation/poc3.mdx | 77 ++ .../project-implementation/swift.mdx | 4 +- .../project-implementation/unity.mdx | 92 +- .../project-implementation/unreal.mdx | 4 +- .../virtual-background/project-setup/web.mdx | 2 +- .../virtual-background/project-test/index.mdx | 10 +- .../virtual-background/project-test/poc3.mdx | 14 + .../project-test/react-js.mdx | 28 + .../virtual-background/project-test/swift.mdx | 27 - .../virtual-background/project-test/unity.mdx | 30 +- .../virtual-background/reference/android.mdx | 9 - .../virtual-background/reference/swift.mdx | 18 - .../virtual-background/reference/unity.mdx | 10 - .../virtual-background/reference/web.mdx | 2 + .../classroom-sdk/android.mdx | 2 +- .../flexible-classroom/classroom-sdk/ios.mdx | 2 +- .../flexible-classroom/classroom-sdk/js.mdx | 16 +- .../customize-ui-scene-sdk/android.mdx | 5 + .../customize-ui-scene-sdk/index.mdx | 4 + .../customize-ui-scene-sdk/ios.mdx | 5 + .../embed-custom-plugin/ios.mdx | 2 +- .../integrate-flexible-classroom-fcr/web.mdx | 33 +- .../integrate-flexible-classroom/android.mdx | 4 +- .../integrate-flexible-classroom/electron.mdx | 55 +- .../integrate-flexible-classroom/ios.mdx | 6 +- .../integrate-flexible-classroom/web.mdx | 77 +- shared/flexible-classroom/proctor-sdk/ios.mdx | 2 +- shared/flexible-classroom/proctor-sdk/js.mdx | 16 +- .../flexible-classroom/quickstart/android.mdx | 2 +- .../quickstart/electron.mdx | 12 +- shared/flexible-classroom/quickstart/web.mdx | 25 +- .../release-notes/android.mdx | 2 +- .../flexible-classroom/release-notes/ios.mdx | 2 +- .../flexible-classroom/release-notes/js.mdx | 37 +- shared/flexible-classroom/ui-scene/js.mdx | 14 + .../fastboard-api/android.mdx | 621 ------------ .../fastboard-api/ios.mdx | 380 -------- .../fastboard-api/web.mdx | 557 ----------- .../get-started-uikit/android.mdx | 8 +- .../get-started-uikit/ios.mdx | 6 +- .../get-started-uikit/web.mdx | 14 +- .../get-started/android.mdx | 2 +- .../get-started/ios.mdx | 2 +- .../get-started/web.mdx | 4 +- .../present-files/android.mdx | 6 +- .../present-files/ios.mdx | 4 +- .../present-files/web.mdx | 12 +- shared/interactive-whiteboard/qps-pricing.mdx | 2 +- .../release-notes-uikit/android.mdx | 57 ++ .../release-notes-uikit/ios.mdx | 43 + .../release-notes-uikit/web.mdx | 86 ++ .../release-notes/android.mdx | 75 +- .../release-notes/ios.mdx | 65 +- .../release-notes/web.mdx | 12 + .../uikit-sdk/android.mdx | 664 ++++++++----- .../interactive-whiteboard/uikit-sdk/ios.mdx | 406 ++++---- .../interactive-whiteboard/uikit-sdk/web.mdx | 184 +++- .../iot/develop/_media-stream-encryption.mdx | 4 +- .../project-implementation/android.mdx | 2 +- .../project-implementation/android.mdx | 24 +- .../get-started-sdk/project-setup/android.mdx | 2 +- .../media-gateway/get-started/_quickstart.mdx | 175 ---- .../reference/_best-practice.mdx | 54 -- .../reference/_release-notes.mdx | 5 - .../get-started/_enable-media-pull.mdx | 2 +- shared/media-push/develop/_restful-api.mdx | 2 +- .../get-started/_enable-media-push.mdx | 2 +- shared/media-push/reference/_pricing.mdx | 4 +- .../_notification_center_service.mdx | 2 +- .../enable-notification-center-service.mdx | 2 +- .../release-notes.mdx | 6 + .../implementation/android.mdx | 5 + .../implementation/index.mdx | 15 + .../implementation/ios.mdx | 4 + .../implementation}/react-native.mdx | 0 .../implementation/unity.mdx | 3 + .../implementation/web.mdx | 161 ++++ .../implementation/windows.mdx | 3 + .../authentication-workflow/index.mdx | 6 +- .../project-implementation/poc3.mdx | 2 +- .../authentication-workflow/test/android.mdx | 5 + .../authentication-workflow/test}/index.mdx | 18 +- .../authentication-workflow/test/ios.mdx | 4 + .../test}/react-native.mdx | 0 .../authentication-workflow/test/unity.mdx | 3 + .../authentication-workflow/test/web.mdx | 34 + .../authentication-workflow/test/windows.mdx | 3 + shared/signaling/beginners-guide/index.mdx | 39 + shared/signaling/cloud-proxy/_cloud-proxy.mdx | 54 ++ shared/signaling/cloud-proxy/index.mdx | 2 +- .../project-implementation/android.mdx | 19 + .../project-implementation/electron.mdx | 34 + .../project-implementation/flutter.mdx | 34 + .../project-implementation/index.mdx | 12 +- .../project-implementation/ios.mdx | 8 + .../project-implementation/macos.mdx | 8 + .../project-implementation/react-native.mdx | 32 + .../project-implementation/swift.mdx | 15 + .../project-implementation/unity.mdx | 19 + .../project-implementation/web.mdx | 15 + .../project-implementation/windows.mdx | 21 + .../cloud-proxy/project-test/android.mdx | 23 + .../cloud-proxy/project-test/electron.mdx} | 12 +- .../cloud-proxy/project-test/flutter.mdx | 23 + .../cloud-proxy/project-test/ios.mdx | 0 .../cloud-proxy/project-test/macos.mdx | 0 .../cloud-proxy/project-test/react-native.mdx | 38 + .../cloud-proxy/project-test/unity.mdx | 17 + .../cloud-proxy/project-test/web.mdx | 7 + .../cloud-proxy/project-test/windows.mdx | 20 + .../data-encryption/_data_encryption.mdx | 62 ++ shared/signaling/data-encryption/index.mdx | 2 +- .../project-implementation/android.mdx | 63 ++ .../project-implementation/electron.mdx | 52 + .../project-implementation/flutter.mdx | 62 ++ .../project-implementation}/index.mdx | 16 +- .../project-implementation/ios.mdx | 1 - .../project-implementation/macos.mdx | 0 .../project-implementation/react-native.mdx | 40 + .../project-implementation/swift.mdx | 49 + .../project-implementation/unity.mdx | 57 ++ .../project-implementation/web.mdx | 70 ++ .../project-implementation/windows.mdx | 58 ++ .../data-encryption/project-test/android.mdx | 24 + .../data-encryption/project-test/electron.mdx | 28 + .../data-encryption/project-test/flutter.mdx | 27 + .../data-encryption}/project-test/ios.mdx | 4 +- .../data-encryption}/project-test/macos.mdx | 4 +- .../project-test/react-native.mdx | 12 + .../data-encryption/project-test/swift.mdx | 17 + .../data-encryption/project-test/unity.mdx | 24 + .../data-encryption/project-test}/web.mdx | 1 + .../data-encryption/project-test/windows.mdx | 36 + shared/signaling/geofencing/index.mdx | 2 +- .../geofencing/project-implement/poc3.mdx | 2 +- .../geofencing/project-test/android.mdx | 23 + .../geofencing/project-test/electron.mdx | 25 + .../geofencing/project-test/flutter.mdx | 23 + .../signaling/geofencing/project-test/ios.mdx | 24 + .../geofencing/project-test/macos.mdx | 20 + .../geofencing/project-test/react-native.mdx | 38 + .../geofencing/project-test/unity.mdx | 17 + .../geofencing/project-test}/web.mdx | 1 + .../geofencing/project-test/windows.mdx | 20 + .../geofencing/reference/android.mdx | 1 - .../signaling/geofencing/reference/index.mdx | 9 - shared/signaling/geofencing/reference/ios.mdx | 1 - .../signaling/geofencing/reference/macos.mdx | 4 +- shared/signaling/geofencing/reference/web.mdx | 2 +- .../get-started-sdk/implementation/poc3.mdx | 7 +- shared/signaling/get-started-sdk/index.mdx | 13 +- .../signaling/get-started-sdk/test/index.mdx | 2 +- shared/signaling/migration-guide/index.mdx | 11 +- shared/signaling/migration-guide/web.mdx | 500 +++++++++- shared/signaling/prerequisites.mdx | 31 +- shared/signaling/presence/index.mdx | 2 +- .../presence/project-implementation/index.mdx | 3 + .../presence/project-implementation/poc3.mdx | 4 +- .../presence/project-implementation/web.mdx | 90 ++ .../signaling/presence/project-test/web.mdx | 34 + .../api-ref/android/_channel-en.android.mdx | 14 +- .../android/_configuration-en.android.mdx | 61 +- .../reference/api-ref/android/_enumv-en.mdx | 15 +- .../api-ref/android/_lock-en.android.mdx | 14 +- .../api-ref/android/_message-en.android.mdx | 11 +- .../api-ref/android/_presence-en.android.mdx | 14 +- .../api-ref/android/_storage-en.android.mdx | 381 ++++---- .../api-ref/android/_topic-en.android.mdx | 34 +- shared/signaling/reference/api-ref/index.mdx | 4 + .../reference/api-ref/ios/_channel-en.ios.mdx | 12 +- .../api-ref/ios/_configuration-en.ios.mdx | 55 +- .../reference/api-ref/ios/_enumv-en.mdx | 15 +- .../reference/api-ref/ios/_lock-en.ios.mdx | 16 +- .../reference/api-ref/ios/_message-en.ios.mdx | 12 +- .../api-ref/ios/_presence-en.ios.mdx | 91 +- .../reference/api-ref/ios/_storage-en.ios.mdx | 81 +- .../reference/api-ref/ios/_topic-en.ios.mdx | 28 +- .../signaling/reference/api-ref/linux-cpp.mdx | 50 + .../api-ref/linux-cpp/_channel-en.cpp.mdx | 342 +++++++ .../linux-cpp/_configuration-en.cpp.mdx | 451 +++++++++ .../reference/api-ref/linux-cpp/_enumv-en.mdx | 261 +++++ .../api-ref/linux-cpp/_lock-en.cpp.mdx | 422 ++++++++ .../api-ref/linux-cpp/_message-en.cpp.mdx | 118 +++ .../api-ref/linux-cpp/_presence-en.cpp.mdx | 392 ++++++++ .../api-ref/linux-cpp/_storage-en.cpp.mdx | 906 ++++++++++++++++++ .../api-ref/linux-cpp/_topic-en.cpp.mdx | 449 +++++++++ .../api-ref/linux-cpp/troubleshooting.mdx | 96 ++ .../reference/api-ref/shared/_channel.mdx | 27 +- .../api-ref/shared/_configuration.mdx | 139 ++- .../reference/api-ref/shared/_enumv.mdx | 169 +++- .../reference/api-ref/shared/_error-codes.mdx | 128 ++- .../reference/api-ref/shared/_lock.mdx | 19 + .../reference/api-ref/shared/_message.mdx | 4 + .../reference/api-ref/shared/_presence.mdx | 56 +- .../api-ref/shared/_rtmstatus-en.mdx | 22 +- .../reference/api-ref/shared/_rtmstatus.mdx | 22 +- .../api-ref/shared/_stateitem-en.mdx | 19 +- .../reference/api-ref/shared/_storage.mdx | 21 + .../reference/api-ref/shared/_topic.mdx | 34 +- .../reference/api-ref/troubleshooting.mdx | 96 ++ shared/signaling/reference/api-ref/unity.mdx | 50 + .../api-ref/unity/_channel-en.unity.mdx | 330 +++++++ .../api-ref/unity/_configuration-en.unity.mdx | 551 +++++++++++ .../reference/api-ref/unity/_enumv-en.mdx | 230 +++++ .../api-ref/unity/_lock-en.unity.mdx | 384 ++++++++ .../api-ref/unity/_message-en.unity.mdx | 97 ++ .../api-ref/unity/_presence-en.unity.mdx | 356 +++++++ .../api-ref/unity/_storage-en.unity.mdx | 851 ++++++++++++++++ .../api-ref/unity/_topic-en.unity.mdx | 349 +++++++ .../api-ref/unity/troubleshooting.mdx | 96 ++ .../api-ref/web/_channel-en.javascript.mdx | 54 +- .../web/_configuration-en.javascript.mdx | 152 +-- .../api-ref/web/_lock-en.javascript.mdx | 102 +- .../api-ref/web/_message-en.javascript.mdx | 30 +- .../api-ref/web/_presence-en.javascript.mdx | 79 +- .../api-ref/web/_storage-en.javascript.mdx | 164 ++-- .../api-ref/web/_topic-en.javascript.mdx | 88 +- shared/signaling/release-notes/android.mdx | 148 +-- shared/signaling/release-notes/index.mdx | 10 +- shared/signaling/release-notes/ios.mdx | 60 +- shared/signaling/release-notes/linux-cpp.mdx | 163 ++++ shared/signaling/release-notes/unity.mdx | 200 ++++ shared/signaling/release-notes/web.mdx | 146 +-- .../run-the-sample-project/unity.mdx | 2 +- shared/signaling/send-gift.mdx | 2 +- shared/signaling/storage/index.mdx | 12 +- .../project-implementation/android.mdx | 340 +++++++ .../storage/project-implementation/index.mdx | 8 + .../storage/project-implementation}/ios.mdx | 3 +- .../storage/project-implementation/macos.mdx | 8 + .../storage/project-implementation/swift.mdx | 339 +++++++ .../storage/project-implementation/web.mdx | 242 +++++ .../storage/project-test}/android.mdx | 0 .../storage/project-test}/electron.mdx | 0 .../storage/project-test}/flutter.mdx | 0 .../signaling/storage/project-test/index.mdx | 13 + .../storage/project-test}/ios.mdx | 0 .../storage/project-test/linux-cpp.mdx | 46 + .../storage/project-test}/macos.mdx | 2 +- .../storage/project-test/react-native.mdx | 3 + .../storage/project-test}/swift.mdx | 0 .../signaling/storage/project-test/unity.mdx | 3 + shared/signaling/storage/project-test/web.mdx | 52 + .../storage/project-test/windows.mdx | 3 + shared/signaling/storage/reference/oc.mdx | 4 +- .../stream-channel/_stream-channel.mdx | 42 + shared/signaling/stream-channel/index.mdx | 11 +- .../project-implementation/android.mdx | 19 + .../project-implementation/electron.mdx | 34 + .../project-implementation/flutter.mdx | 34 + .../project-implementation}/index.mdx | 13 +- .../project-implementation/ios.mdx | 8 + .../project-implementation/macos.mdx | 8 + .../project-implementation/poc3.mdx | 4 +- .../project-implementation/react-native.mdx | 32 + .../project-implementation/swift.mdx | 15 + .../project-implementation/unity.mdx | 19 + .../project-implementation/web.mdx | 99 ++ .../project-implementation/windows.mdx | 21 + .../stream-channel/project-test/android.mdx | 23 + .../stream-channel/project-test/electron.mdx | 25 + .../stream-channel/project-test/flutter.mdx | 23 + .../stream-channel/project-test/ios.mdx | 24 + .../stream-channel/project-test/macos.mdx | 20 + .../project-test/react-native.mdx | 38 + .../stream-channel/project-test/unity.mdx | 17 + .../stream-channel/project-test/web.mdx | 11 + .../stream-channel/project-test/windows.mdx | 20 + .../stream-channel/reference/android.mdx | 2 - .../stream-channel/reference/electron.mdx | 16 - .../stream-channel/reference/flutter.mdx | 14 - .../stream-channel/reference/ios.mdx | 3 - .../stream-channel/reference/macos.mdx | 4 - .../stream-channel/reference/react-native.mdx | 11 - .../stream-channel/reference/unity.mdx | 13 - .../stream-channel/reference/windows.mdx | 4 - shared/variables/global.js | 37 +- shared/variables/product.js | 8 - shared/video-sdk/_authentication-workflow.mdx | 139 ++- shared/video-sdk/_billing.mdx | 8 +- shared/video-sdk/_get-started-uikit.mdx | 3 +- .../project-implementation-uikit/android.mdx | 6 +- .../react-native.mdx | 2 +- .../project-implementation-uikit/swift.mdx | 4 +- .../project-implementation-uikit/web.mdx | 2 +- .../project-implementation/android.mdx | 138 +-- .../project-implementation/cpp.mdx | 281 ++++-- .../project-implementation/csharp.mdx | 138 ++- .../project-implementation/electron.mdx | 16 +- .../project-implementation/flutter.mdx | 8 +- .../project-implementation/index.mdx | 15 +- .../project-implementation/poc3.mdx | 38 + .../project-implementation/react-js.mdx | 6 +- .../project-implementation/react-native.mdx | 92 +- .../project-implementation/swift.mdx | 53 +- .../project-implementation/unity.mdx | 4 +- .../project-implementation/unreal.mdx | 3 +- .../project-implementation/web.mdx | 80 -- .../project-test/android.mdx | 2 +- .../project-test/cpp.mdx | 21 +- .../project-test/electron.mdx | 2 +- .../project-test/flutter.mdx | 2 +- .../project-test/index.mdx | 34 +- .../project-test/poc3.mdx | 38 + .../project-test/react-js.mdx | 41 +- .../project-test/react-native.mdx | 41 +- .../project-test/swift.mdx | 2 +- .../project-test/unity.mdx | 7 +- .../project-test/unreal.mdx | 2 +- .../project-test/web.mdx | 2 +- .../reference/android.mdx | 9 +- .../authentication-workflow/reference/ios.mdx | 3 - .../reference/macos.mdx | 3 - .../reference/react-js.mdx | 2 - .../reference/unity.mdx | 4 - .../reference/unreal.mdx | 3 +- .../authentication-workflow/reference/web.mdx | 2 - .../develop/_audio-and-voice-effects.mdx | 15 +- shared/video-sdk/develop/_cloud-proxy.mdx | 30 +- .../develop/_custom-video-and-audio.mdx | 21 +- .../develop/_ensure-channel-quality.mdx | 9 +- shared/video-sdk/develop/_geofencing.mdx | 17 +- ..._live-streaming-over-multiple-channels.mdx | 17 +- .../develop/_media-stream-encryption.mdx | 44 +- shared/video-sdk/develop/_migration-guide.mdx | 1 - shared/video-sdk/develop/_play-media.mdx | 12 +- .../video-sdk/develop/_product-workflow.mdx | 24 +- .../develop/_real-time-transcription.mdx | 220 ----- .../video-sdk/develop/_screenshot-upload.mdx | 4 +- shared/video-sdk/develop/_spatial-audio.mdx | 19 +- .../develop/_stream-raw-audio-and-video.mdx | 30 +- .../video-sdk/develop/_video-compositor.mdx | 56 -- .../project-implementation/android.mdx | 2 +- .../project-implementation/electron.mdx | 2 +- .../project-implementation/flutter.mdx | 20 +- .../project-implementation/index.mdx | 13 +- .../project-implementation/ios.mdx | 2 +- .../project-implementation/poc3.mdx | 60 ++ .../project-implementation/react-native.mdx | 20 +- .../project-implementation/swift.mdx | 16 +- .../project-implementation/unity.mdx | 359 +++---- .../project-implementation/unreal.mdx | 2 + .../project-test/android.mdx | 34 +- .../project-test/index.mdx | 28 +- .../project-test/poc3.mdx | 19 + .../project-test/react-js.mdx | 26 + .../project-test/swift.mdx | 36 +- .../project-test/unity.mdx | 34 +- .../reference/android.mdx | 45 - .../audio-and-voice-effects/reference/ios.mdx | 66 -- .../reference/macos.mdx | 63 -- .../reference/react-js.mdx | 2 - .../reference/unity.mdx | 37 - .../reference/unreal.mdx | 2 +- .../audio-and-voice-effects/reference/web.mdx | 33 +- .../project-implementation/android.mdx | 8 +- .../project-implementation/electron.mdx | 6 +- .../project-implementation/flutter.mdx | 6 +- .../project-implementation/index.mdx | 16 +- .../project-implementation/poc3.mdx | 45 + .../project-implementation/react-native.mdx | 6 +- .../project-implementation/unity.mdx | 68 +- .../project-implementation/unreal.mdx | 2 +- .../project-implementation/web.mdx | 44 - .../project-setup/unity.mdx | 0 .../cloud-proxy/project-test/index.mdx | 24 +- .../develop/cloud-proxy/project-test/poc3.mdx | 13 + .../cloud-proxy/project-test/react-js.mdx | 26 + .../cloud-proxy/project-test/unity.mdx | 27 +- .../cloud-proxy/project-test/unreal.mdx | 2 +- .../develop/cloud-proxy/project-test/web.mdx | 37 +- .../develop/cloud-proxy/reference/android.mdx | 13 - .../develop/cloud-proxy/reference/index.mdx | 3 +- .../develop/cloud-proxy/reference/ios.mdx | 9 - .../develop/cloud-proxy/reference/macos.mdx | 9 - .../cloud-proxy/reference/react-js.mdx | 3 - .../develop/cloud-proxy/reference/unity.mdx | 13 - .../develop/cloud-proxy/reference/web.mdx | 9 +- .../project-implementation/index.mdx | 16 +- .../project-implementation/poc3.mdx | 182 ++++ .../project-implementation/react-js.mdx | 6 +- .../project-implementation/unity.mdx | 2 +- .../project-implementation/unreal.mdx | 3 +- .../project-implementation/web.mdx | 53 - .../project-implementation/windows.mdx | 2 +- .../project-test/android.mdx | 17 +- .../project-test/index.mdx | 28 +- .../project-test/poc3.mdx | 20 + .../project-test/react-js.mdx | 21 + .../project-test/swift.mdx | 22 +- .../project-test/unity.mdx | 26 +- .../project-test/unreal.mdx | 2 +- .../project-test/web.mdx | 25 +- .../reference/android.mdx | 12 - .../reference/index.mdx | 1 - .../custom-video-and-audio/reference/ios.mdx | 13 - .../reference/macos.mdx | 14 - .../reference/react-js.mdx | 2 - .../reference/unity.mdx | 10 - .../reference/unreal.mdx | 2 +- .../custom-video-and-audio/reference/web.mdx | 10 +- .../project-implementation/android.mdx | 8 +- .../project-implementation/electron.mdx | 4 +- .../project-implementation/flutter.mdx | 8 +- .../project-implementation/index.mdx | 18 +- .../project-implementation/poc3.mdx | 42 + .../project-implementation/swift.mdx | 6 +- .../project-implementation/unity.mdx | 71 +- .../project-implementation/unreal.mdx | 1 + .../project-implementation/web.mdx | 312 ------ .../project-implementation/windows.mdx | 2 +- .../project-test/android.mdx | 16 +- .../project-test/electron.mdx | 2 +- .../project-test/index.mdx | 52 +- .../project-test/poc3.mdx | 59 ++ .../project-test/react-js.mdx | 26 + .../project-test/swift.mdx | 8 +- .../project-test/unity.mdx | 31 +- .../project-test/unreal.mdx | 4 +- .../project-test/web.mdx | 16 +- .../reference/android.mdx | 12 - .../encrypt-media-streams/reference/index.mdx | 2 +- .../encrypt-media-streams/reference/ios.mdx | 4 - .../encrypt-media-streams/reference/macos.mdx | 5 - .../reference/react-js.mdx | 3 - .../encrypt-media-streams/reference/unity.mdx | 4 - .../reference/unreal.mdx | 7 +- .../encrypt-media-streams/reference/web.mdx | 12 - .../project-implementation/android.mdx | 20 +- .../project-implementation/electron.mdx | 4 +- .../project-implementation/flutter.mdx | 20 +- .../project-implementation/index.mdx | 17 +- .../project-implementation/poc3.mdx | 104 ++ .../project-implementation/react-js.mdx | 105 +- .../project-implementation/react-native.mdx | 616 ++++++------ .../project-implementation/swift.mdx | 6 +- .../project-implementation/unity.mdx | 459 +++++---- .../project-implementation/unreal.mdx | 1 + .../project-implementation/web.mdx | 252 ----- .../project-implementation/windows.mdx | 403 ++++++-- .../project-test/android.mdx | 11 +- .../project-test/index.mdx | 29 +- .../project-test/poc3.mdx | 20 + .../project-test/react-js.mdx | 45 + .../project-test/react-native.mdx | 67 +- .../project-test/swift.mdx | 33 +- .../project-test/unity.mdx | 20 +- .../project-test/unreal.mdx | 3 +- .../project-test/web.mdx | 28 +- .../reference/android.mdx | 31 - .../reference/react-js.mdx | 1 - .../reference/swift.mdx | 21 - .../reference/unity.mdx | 47 - .../ensure-channel-quality/reference/web.mdx | 23 +- .../project-implementation/electron.mdx | 2 +- .../project-implementation/flutter.mdx | 2 +- .../project-implementation/index.mdx | 19 +- .../project-implementation/poc3.mdx | 24 + .../project-implementation/unity.mdx | 39 +- .../geofencing/project-implementation/web.mdx | 17 - .../geofencing/project-test/android.mdx | 10 +- .../geofencing/project-test/flutter.mdx | 2 +- .../develop/geofencing/project-test/index.mdx | 27 +- .../develop/geofencing/project-test/poc3.mdx | 11 + .../geofencing/project-test/react-js.mdx | 31 + .../develop/geofencing/project-test/unity.mdx | 41 +- .../geofencing/project-test/unreal.mdx | 3 +- .../develop/geofencing/project-test/web.mdx | 15 +- .../develop/geofencing/reference/android.mdx | 20 - .../develop/geofencing/reference/react-js.mdx | 3 - .../develop/geofencing/reference/swift.mdx | 47 +- .../develop/geofencing/reference/unity.mdx | 11 - .../develop/geofencing/reference/web.mdx | 12 +- .../project-implementation/cpp.mdx | 4 +- .../project-implementation/csharp.mdx | 4 +- .../project-implementation/java.mdx | 50 +- .../project-implementation/nodejs.mdx | 6 +- .../project-implementation/python.mdx | 8 +- .../project-implementation/unreal.mdx | 2 +- .../project-implementation/web.mdx | 24 +- .../project-setup/unreal.mdx | 2 +- .../project-test/python.mdx | 2 +- .../project-test/unreal.mdx | 2 +- .../reference/unreal.mdx | 2 +- .../project-implementation/android.mdx | 38 +- .../project-implementation/electron.mdx | 24 +- .../project-implementation/flutter.mdx | 24 +- .../project-implementation/index.mdx | 13 +- .../project-implementation/poc3.mdx | 40 + .../project-implementation/react-native.mdx | 24 +- .../project-implementation/swift.mdx | 16 +- .../project-implementation/unity.mdx | 401 ++++---- .../project-implementation/unreal.mdx | 3 + .../project-implementation/web.mdx | 249 ----- .../project-implementation/windows.mdx | 20 +- .../project-test/android.mdx | 61 +- .../project-test/index.mdx | 22 +- .../project-test/ios.mdx | 60 +- .../project-test/macos.mdx | 8 +- .../project-test/poc3.mdx | 20 + .../project-test/react-js.mdx | 89 ++ .../project-test/swift.mdx | 56 -- .../project-test/unity.mdx | 60 +- .../project-test/web.mdx | 91 +- .../reference/android.mdx | 14 - .../reference/ios.mdx | 11 - .../reference/macos.mdx | 11 - .../reference/unity.mdx | 14 - .../reference/web.mdx | 13 - .../develop/migration-guide/unreal.mdx | 2 +- .../video-sdk/develop/migration-guide/web.mdx | 20 +- .../project-implementation/android.mdx | 353 +++---- .../project-implementation/electron.mdx | 16 +- .../project-implementation/flutter.mdx | 16 +- .../project-implementation/index.mdx | 15 +- .../project-implementation/poc3.mdx | 76 ++ .../project-implementation/react-native.mdx | 20 +- .../project-implementation/swift.mdx | 12 +- .../project-implementation/unity.mdx | 215 ++--- .../project-implementation/unreal.mdx | 4 + .../play-media/project-implementation/web.mdx | 48 - .../project-implementation/windows.mdx | 30 +- .../play-media/project-test/android.mdx | 40 +- .../develop/play-media/project-test/index.mdx | 29 +- .../develop/play-media/project-test/poc3.mdx | 20 + .../play-media/project-test/react-js.mdx | 23 + .../develop/play-media/project-test/swift.mdx | 38 +- .../develop/play-media/project-test/unity.mdx | 40 +- .../play-media/project-test/unreal.mdx | 3 +- .../develop/play-media/project-test/web.mdx | 26 +- .../develop/play-media/reference/android.mdx | 9 - .../develop/play-media/reference/react-js.mdx | 3 - .../develop/play-media/reference/swift.mdx | 19 - .../develop/play-media/reference/unity.mdx | 8 - .../develop/play-media/reference/web.mdx | 6 +- .../project-implementation/android.mdx | 6 +- .../project-implementation/electron.mdx | 2 +- .../project-implementation/flutter.mdx | 4 +- .../project-implementation/index.mdx | 14 +- .../project-implementation/macos.mdx | 2 +- .../project-implementation/poc3.mdx | 104 ++ .../project-implementation/react-native.mdx | 2 +- .../project-implementation/swift.mdx | 4 +- .../project-implementation/unity.mdx | 276 +++--- .../project-implementation/unreal.mdx | 7 + .../project-implementation/web.mdx | 203 ---- .../project-setup/android.mdx | 16 +- .../project-setup/flutter.mdx | 2 +- .../product-workflow/project-test/android.mdx | 51 +- .../product-workflow/project-test/index.mdx | 22 +- .../product-workflow/project-test/ios.mdx | 52 +- .../product-workflow/project-test/poc3.mdx | 20 + .../project-test/react-js.mdx | 35 + .../product-workflow/project-test/swift.mdx | 68 +- .../product-workflow/project-test/unity.mdx | 56 +- .../product-workflow/project-test/unreal.mdx | 2 +- .../product-workflow/project-test/web.mdx | 53 +- .../product-workflow/project-test/windows.mdx | 4 +- .../product-workflow/reference/android.mdx | 22 - .../product-workflow/reference/react-js.mdx | 3 - .../product-workflow/reference/swift.mdx | 29 - .../product-workflow/reference/unity.mdx | 22 +- .../product-workflow/reference/web.mdx | 23 +- .../project-implementation/android.mdx | 399 -------- .../project-implementation/electron.mdx | 360 ------- .../project-implementation/flutter.mdx | 311 ------ .../project-implementation/ios.mdx | 384 -------- .../project-implementation/macos.mdx | 386 -------- .../project-implementation/react-native.mdx | 338 ------- .../project-implementation/unity.mdx | 372 ------- .../project-implementation/web.mdx | 360 ------- .../project-implementation/windows.mdx | 493 ---------- .../project-test/android.mdx | 31 - .../project-test/electron.mdx | 31 - .../project-test/flutter.mdx | 31 - .../project-test/react-native.mdx | 29 - .../project-test/swift.mdx | 28 - .../project-test/unity.mdx | 27 - .../project-test/web.mdx | 33 - .../project-test/windows.mdx | 27 - .../reference/android.mdx | 7 - .../reference/electron.mdx | 7 - .../reference/flutter.mdx | 8 - .../reference/index.mdx | 24 - .../real-time-transcription/reference/ios.mdx | 7 - .../reference/macos.mdx | 7 - .../reference/swift.mdx | 0 .../reference/unity.mdx | 7 - .../reference/windows.mdx | 7 - .../project-implementation/index.mdx | 12 +- .../project-implementation/poc3.mdx | 73 ++ .../project-implementation/swift.mdx | 112 --- .../project-implementation/unity.mdx | 4 +- .../project-implementation/unreal.mdx | 4 + .../project-implementation/web.mdx | 181 ---- .../spatial-audio/project-setup/swift.mdx | 8 +- .../spatial-audio/project-test/android.mdx | 23 +- .../spatial-audio/project-test/index.mdx | 19 +- .../spatial-audio/project-test/poc3.mdx | 20 + .../spatial-audio/project-test/react-js.mdx | 25 + .../spatial-audio/project-test/swift.mdx | 26 +- .../spatial-audio/project-test/unity.mdx | 25 +- .../spatial-audio/project-test/unreal.mdx | 2 +- .../spatial-audio/project-test/web.mdx | 35 +- .../spatial-audio/reference/android.mdx | 14 - .../develop/spatial-audio/reference/ios.mdx | 14 - .../develop/spatial-audio/reference/macos.mdx | 14 - .../develop/spatial-audio/reference/unity.mdx | 28 - .../spatial-audio/reference/unreal.mdx | 2 +- .../develop/spatial-audio/reference/web.mdx | 4 +- .../project-implementation/android.mdx | 16 +- .../project-implementation/electron.mdx | 10 +- .../project-implementation/flutter.mdx | 8 +- .../project-implementation/index.mdx | 14 +- .../project-implementation/ios.mdx | 16 +- .../project-implementation/poc3.mdx | 52 + .../project-implementation/react-native.mdx | 12 +- .../project-implementation/swift.mdx | 6 +- .../project-implementation/unity.mdx | 16 +- .../project-test/android.mdx | 30 +- .../project-test/electron.mdx | 4 - .../project-test/flutter.mdx | 4 - .../project-test/index.mdx | 20 +- .../project-test/ios.mdx | 67 +- .../project-test/poc3.mdx | 16 + .../project-test/react-native.mdx | 14 +- .../project-test/swift.mdx | 47 +- .../project-test/unity.mdx | 33 +- .../project-test/unreal.mdx | 10 +- .../project-test/windows.mdx | 5 +- .../reference/android.mdx | 12 - .../reference/ios.mdx | 13 - .../reference/macos.mdx | 14 - .../reference/unity.mdx | 15 - .../reference/unreal.mdx | 2 +- .../project-implementation/index.mdx | 3 - .../project-implementation/web.mdx | 130 --- .../video-compositor/project-setup/index.mdx | 3 - .../video-compositor/project-setup/web.mdx | 20 - .../video-compositor/project-test/index.mdx | 3 - .../video-compositor/reference/index.mdx | 3 - .../video-compositor/reference/web.mdx | 61 -- .../get-started/get-started-sdk/index.mdx | 65 +- .../project-implementation/android.mdx | 239 ----- .../project-implementation/csharp.mdx | 47 +- .../project-implementation/electron.mdx | 8 +- .../project-implementation/flutter.mdx | 228 ----- .../project-implementation/index.mdx | 22 +- .../project-implementation/poc3.mdx | 200 ++++ .../project-implementation/react-js.mdx | 1 - .../project-implementation/react-native.mdx | 583 ++++++++--- .../project-implementation/swift.mdx | 81 -- .../project-implementation/unity.mdx | 267 ++++-- .../project-implementation/unreal.mdx | 11 +- .../project-implementation/web.mdx | 131 --- .../project-implementation/windows.mdx | 241 +++-- .../project-setup/react-js.mdx | 3 + .../get-started-sdk/project-test/android.mdx | 2 +- .../get-started-sdk/project-test/flutter.mdx | 2 +- .../get-started-sdk/project-test/index.mdx | 34 +- .../get-started-sdk/project-test/poc3.mdx | 47 + .../get-started-sdk/project-test/react-js.mdx | 15 +- .../project-test/react-native.mdx | 49 +- .../get-started-sdk/project-test/swift.mdx | 4 +- .../get-started-sdk/project-test/unity.mdx | 11 +- .../get-started-sdk/project-test/unreal.mdx | 1 + .../get-started-sdk/project-test/web.mdx | 2 +- .../get-started-sdk/project-test/windows.mdx | 12 +- .../get-started-sdk/reference/android.mdx | 10 - .../get-started-sdk/reference/ios.mdx | 12 - .../get-started-sdk/reference/macos.mdx | 13 +- .../get-started-sdk/reference/react-js.mdx | 2 - .../get-started-sdk/reference/unity.mdx | 18 - .../get-started-sdk/reference/web.mdx | 12 +- .../get-started-sdk/reference/windows.mdx | 10 - .../project-setup/android.mdx | 2 +- .../project-setup/flutter.mdx | 2 +- .../get-started-uikit/project-setup/web.mdx | 2 +- .../project-test/flutter.mdx | 2 +- .../reference/_app-size-optimization.mdx | 2 +- shared/video-sdk/reference/_error-codes.mdx | 80 ++ shared/video-sdk/reference/_known-issues.mdx | 44 - shared/video-sdk/reference/_pricing.mdx | 4 +- shared/video-sdk/reference/_release-notes.mdx | 5 +- .../video-sdk/reference/_service_limits.mdx | 18 + .../manual-install/electron.mdx | 2 +- .../manual-install/flutter.mdx | 2 +- .../manual-install/index.mdx | 6 +- .../manual-install/ios.mdx | 2 +- .../manual-install/macos.mdx | 2 +- .../manual-install/react-js.mdx | 2 +- .../manual-install/unreal.mdx | 2 +- .../manual-install/web.mdx | 2 +- .../project-implementation/index.mdx | 6 +- .../project-implementation/unreal.mdx | 3 - .../reference/plugin-descriptions.mdx | 2 +- .../reference/known-issues/android.mdx | 11 +- .../reference/known-issues/flutter.mdx | 19 +- .../video-sdk/reference/known-issues/ios.mdx | 10 +- .../reference/known-issues/react-native.mdx | 19 +- .../reference/known-issues/unity.mdx | 18 +- .../video-sdk/reference/known-issues/web.mdx | 532 +++++----- .../reference/release-notes/android.mdx | 254 ++++- .../reference/release-notes/flutter.mdx | 22 +- .../video-sdk/reference/release-notes/ios.mdx | 253 ++++- .../reference/release-notes/macos.mdx | 210 +++- .../reference/release-notes/react-js.mdx | 69 +- .../reference/release-notes/react-native.mdx | 22 +- .../reference/release-notes/unity.mdx | 22 +- .../video-sdk/reference/release-notes/web.mdx | 111 ++- .../reference/release-notes/windows.mdx | 218 ++++- .../understand/_product-overview.mdx | 2 +- .../index.mdx} | 4 +- .../project-implementation/android.mdx | 24 +- .../project-implementation/electron.mdx | 16 +- .../project-implementation/react-native.mdx | 2 +- .../project-implementation/unity.mdx | 16 +- .../project-implementation/unreal.mdx | 4 +- .../project-implementation/web.mdx | 22 +- .../project-test/electron.mdx | 2 +- .../project-test/unreal.mdx | 2 +- .../project-test/web.mdx | 2 +- .../reference/unreal.mdx | 3 +- shared/voice-sdk/develop/_custom-audio.mdx | 4 +- .../voice-sdk/develop/_stream-raw-audio.mdx | 21 +- .../reference/unreal.mdx | 23 - .../project-implementation/index.mdx | 1 - .../project-implementation/web.mdx | 2 +- .../custom-audio-deprecated/reference/web.mdx | 4 - .../index.mdx} | 9 - .../project-implementation/index.mdx | 12 +- .../project-implementation/poc3.mdx | 67 ++ .../project-test/.web.mdx | 28 + .../project-test/android.mdx | 43 +- .../project-test/index.mdx | 20 +- .../project-test/poc3.mdx | 20 + .../project-test/react-js.mdx | 45 + .../project-test/swift.mdx | 31 +- .../project-test/unity.mdx | 51 +- .../project-test/unreal.mdx | 3 +- .../project-test/web.mdx | 35 +- .../project-test/windows.mdx | 59 ++ .../reference/android.mdx | 24 - .../reference/macos.mdx | 1 - .../reference/swift.mdx | 12 - .../reference/unity.mdx | 19 - .../ensure-channel-quality/reference/web.mdx | 10 - .../project-implementation/flutter.mdx | 2 +- .../geofencing/project-test/flutter.mdx | 2 +- .../index.mdx} | 9 +- .../project-implementation/android.mdx | 2 +- .../project-implementation/flutter.mdx | 2 +- .../project-implementation/index.mdx | 12 +- .../project-implementation/poc3.mdx | 32 + .../project-implementation/swift.mdx | 2 +- .../project-implementation/unity.mdx | 4 +- .../project-implementation/unreal.mdx | 2 + .../project-implementation/web.mdx | 102 -- .../product-workflow/project-test/index.mdx | 1 - .../product-workflow/project-test/unreal.mdx | 2 +- .../product-workflow/project-test/windows.mdx | 2 +- .../product-workflow/reference/android.mdx | 15 - .../product-workflow/reference/swift.mdx | 36 - .../product-workflow/reference/unity.mdx | 16 - .../product-workflow/reference/unreal.mdx | 18 +- .../product-workflow/reference/web.mdx | 6 - .../project-implementation/electron.mdx | 8 +- .../project-implementation/flutter.mdx | 6 +- .../project-implementation/index.mdx | 10 +- .../project-implementation/poc3.mdx | 47 + .../project-implementation/react-native.mdx | 10 +- .../project-implementation/unity.mdx | 6 +- .../stream-raw-audio/reference/android.mdx | 7 - .../stream-raw-audio/reference/ios.mdx | 6 - .../stream-raw-audio/reference/macos.mdx | 7 - .../stream-raw-audio/reference/unity.mdx | 8 - .../index.mdx} | 59 +- .../project-implementation/android.mdx | 22 +- .../project-implementation/electron.mdx | 2 +- .../project-implementation/flutter.mdx | 14 +- .../project-implementation/index.mdx | 13 +- .../project-implementation/poc3.mdx | 106 ++ .../project-implementation/swift.mdx | 12 +- .../project-implementation/unity.mdx | 26 +- .../project-implementation/unreal.mdx | 4 + .../project-implementation/web.mdx | 2 +- .../project-implementation/windows.mdx | 2 +- .../get-started-sdk/project-setup/android.mdx | 68 +- .../project-setup/electron.mdx | 4 +- .../get-started-sdk/project-setup/flutter.mdx | 2 +- .../get-started-sdk/project-setup/windows.mdx | 2 +- .../get-started-sdk/project-test/index.mdx | 25 +- .../get-started-sdk/reference/android.mdx | 6 - .../get-started-sdk/reference/ios.mdx | 6 - .../get-started-sdk/reference/macos.mdx | 6 - .../get-started-sdk/reference/unity.mdx | 12 - .../get-started-sdk/reference/unreal.mdx | 2 +- .../get-started-sdk/reference/web.mdx | 1 - shared/voice-sdk/reference/_known-issues.mdx | 42 - .../voice-sdk/reference/_service_limits.mdx | 21 + .../reference/known-issues/flutter.mdx | 10 +- .../voice-sdk/reference/known-issues/ios.mdx | 10 +- .../reference/known-issues/react-native.mdx | 10 +- .../reference/known-issues/unity.mdx | 10 +- .../reference/release-notes/android.mdx | 187 +++- .../reference/release-notes/flutter.mdx | 15 + .../reference/release-notes/index.mdx | 2 + .../voice-sdk/reference/release-notes/ios.mdx | 208 +++- .../reference/release-notes/macos.mdx | 188 ++++ .../reference/release-notes/react-js.mdx | 65 ++ .../reference/release-notes/react-native.mdx | 15 + .../reference/release-notes/unity.mdx | 15 + .../reference/release-notes/windows.mdx | 196 ++++ signaling/develop/cloud-proxy.mdx | 2 +- signaling/develop/data-encryption.mdx | 2 +- .../get-started-sdk.mdx | 0 signaling/develop/locks.mdx | 31 + signaling/develop/presence.mdx | 2 +- signaling/develop/storage.mdx | 4 +- .../stream-channel.mdx | 0 signaling/develop/topics.mdx | 45 + signaling/get-started/beginners-guide.mdx | 13 + .../integrate-token-generation.mdx | 2 +- signaling/get-started/migration-guide.mdx | 13 + signaling/overview/pricing.mdx | 48 + signaling/overview/product-overview.mdx | 12 +- .../{reference => overview}/release-notes.mdx | 2 +- .../supported-platforms.mdx | 0 signaling/reference/api.mdx | 30 +- signaling/reference/downloads.mdx | 8 +- signaling/reference/limitations.mdx | 4 + signaling/reference/pricing.mdx | 190 ---- signaling/reference/restful-messaging.mdx | 261 +++++ signaling/reference/user-channel-events.mdx | 13 + .../develop/media-stream-encryption.mdx | 2 +- .../enable-features/image-enhancement.mdx | 13 - .../enable-features/screenshot-upload.mdx | 2 +- .../enable-features/video-compositor.mdx | 13 - video-calling/get-started/get-started-sdk.mdx | 1 - .../{reference => overview}/pricing.mdx | 2 +- .../{reference => overview}/release-notes.mdx | 2 +- .../supported-platforms.mdx | 0 video-calling/reference/error-codes.mdx | 13 + video-calling/reference/known-issues.mdx | 14 - video-calling/reference/service-limits.mdx | 13 + voice-calling/develop/cloud-proxy.mdx | 6 +- .../develop/ensure-channel-quality.mdx | 2 +- .../develop/media-stream-encryption.mdx | 4 +- voice-calling/develop/product-workflow.mdx | 2 +- .../get-started/authentication-workflow.mdx | 6 +- voice-calling/get-started/get-started-sdk.mdx | 6 +- .../{reference => overview}/pricing.mdx | 2 +- .../{reference => overview}/release-notes.mdx | 2 +- .../supported-platforms.mdx | 2 +- voice-calling/reference/error-codes.mdx | 13 + voice-calling/reference/known-issues.mdx | 15 - voice-calling/reference/service-limits.mdx | 13 + 1557 files changed, 42819 insertions(+), 41122 deletions(-) create mode 100644 .gitignore rename agora-analytics/{reference => overview}/pricing.mdx (94%) rename agora-analytics/{reference => overview}/release-notes.mdx (93%) rename agora-analytics/{reference => overview}/supported-platforms.mdx (78%) rename agora-chat/{reference => overview}/pricing.mdx (100%) rename agora-chat/{reference => overview}/release-notes.mdx (93%) rename agora-chat/{reference => overview}/supported-platforms.mdx (93%) create mode 100644 assets/code/video-sdk/ai-noise-suppression/configure-engine.mdx create mode 100644 assets/code/video-sdk/ai-noise-suppression/configure-extension.mdx create mode 100644 assets/code/video-sdk/ai-noise-suppression/enable-denoiser.mdx create mode 100644 assets/code/video-sdk/ai-noise-suppression/import-library.mdx create mode 100644 assets/code/video-sdk/ai-noise-suppression/import-plugin.mdx create mode 100644 assets/code/video-sdk/ai-noise-suppression/set-noise-reduction-mode.mdx create mode 100644 assets/code/video-sdk/ai-noise-suppression/set-reduction-level.mdx create mode 100644 assets/code/video-sdk/ai-noise-suppression/setup-logging.mdx create mode 100644 assets/code/video-sdk/audio-voice-effects/apply-voice-effects.mdx rename assets/code/video-sdk/audio-voice-effects/{swift => }/configure-buttons.mdx (99%) create mode 100644 assets/code/video-sdk/audio-voice-effects/configure-engine.mdx create mode 100644 assets/code/video-sdk/audio-voice-effects/create-ui.mdx create mode 100644 assets/code/video-sdk/audio-voice-effects/event-handler.mdx create mode 100644 assets/code/video-sdk/audio-voice-effects/import-library.mdx create mode 100644 assets/code/video-sdk/audio-voice-effects/pause-play-resume.mdx create mode 100644 assets/code/video-sdk/audio-voice-effects/preload-effect.mdx create mode 100644 assets/code/video-sdk/audio-voice-effects/set-audio-profile.mdx create mode 100644 assets/code/video-sdk/audio-voice-effects/set-audio-route.mdx create mode 100644 assets/code/video-sdk/audio-voice-effects/set-variables.mdx create mode 100644 assets/code/video-sdk/audio-voice-effects/stop-start-mixing.mdx delete mode 100644 assets/code/video-sdk/audio-voice-effects/swift/apply-voice-effects.mdx delete mode 100644 assets/code/video-sdk/audio-voice-effects/swift/create-ui.mdx delete mode 100644 assets/code/video-sdk/audio-voice-effects/swift/pause-play-resume.mdx delete mode 100644 assets/code/video-sdk/audio-voice-effects/swift/set-audio-route.mdx delete mode 100644 assets/code/video-sdk/audio-voice-effects/swift/stop-start-mixing.mdx rename assets/code/video-sdk/audio-voice-effects/{swift => }/update-ui.mdx (100%) create mode 100644 assets/code/video-sdk/authentication-workflow/add-variables.mdx create mode 100644 assets/code/video-sdk/authentication-workflow/event-handler.mdx create mode 100644 assets/code/video-sdk/authentication-workflow/fetch-token.mdx create mode 100644 assets/code/video-sdk/authentication-workflow/import-library.mdx create mode 100644 assets/code/video-sdk/authentication-workflow/join-channel.mdx create mode 100644 assets/code/video-sdk/authentication-workflow/renew-token.mdx rename assets/code/video-sdk/authentication-workflow/{swift => }/specify-channel.mdx (95%) delete mode 100644 assets/code/video-sdk/authentication-workflow/swift/add-variables.mdx delete mode 100644 assets/code/video-sdk/authentication-workflow/swift/fetch-token.mdx delete mode 100644 assets/code/video-sdk/authentication-workflow/swift/join-channel.mdx create mode 100644 assets/code/video-sdk/cloud-proxy/configure-engine.mdx create mode 100644 assets/code/video-sdk/cloud-proxy/connection-failed.mdx create mode 100644 assets/code/video-sdk/cloud-proxy/event-handler.mdx create mode 100644 assets/code/video-sdk/cloud-proxy/import-library.mdx create mode 100644 assets/code/video-sdk/cloud-proxy/set-cloud-proxy.mdx create mode 100644 assets/code/video-sdk/cloud-proxy/set-variables.mdx create mode 100644 assets/code/video-sdk/custom-video-and-audio/configure-engine-audio.mdx create mode 100644 assets/code/video-sdk/custom-video-and-audio/configure-engine.mdx create mode 100644 assets/code/video-sdk/custom-video-and-audio/create-custom-audio-track.mdx create mode 100644 assets/code/video-sdk/custom-video-and-audio/create-custom-video-track.mdx create mode 100644 assets/code/video-sdk/custom-video-and-audio/destroy-custom-track-audio.mdx create mode 100644 assets/code/video-sdk/custom-video-and-audio/destroy-custom-track-video.mdx create mode 100644 assets/code/video-sdk/custom-video-and-audio/enable-audio-publishing.mdx create mode 100644 assets/code/video-sdk/custom-video-and-audio/enable-video-publishing.mdx create mode 100644 assets/code/video-sdk/custom-video-and-audio/import-library-audio.mdx create mode 100644 assets/code/video-sdk/custom-video-and-audio/import-library.mdx create mode 100644 assets/code/video-sdk/custom-video-and-audio/push-audio-frames.mdx create mode 100644 assets/code/video-sdk/custom-video-and-audio/push-video-frames.mdx create mode 100644 assets/code/video-sdk/custom-video-and-audio/read-audio-input.mdx create mode 100644 assets/code/video-sdk/custom-video-and-audio/render-custom-video.mdx create mode 100644 assets/code/video-sdk/custom-video-and-audio/set-variables-audio.mdx create mode 100644 assets/code/video-sdk/custom-video-and-audio/set-variables.mdx create mode 100644 assets/code/video-sdk/encrypt-media-streams/enable-encryption.mdx create mode 100644 assets/code/video-sdk/encrypt-media-streams/enable-end-to-end-encryption.mdx create mode 100644 assets/code/video-sdk/encrypt-media-streams/event-handler.mdx create mode 100644 assets/code/video-sdk/encrypt-media-streams/import-library.mdx create mode 100644 assets/code/video-sdk/encrypt-media-streams/set-variables.mdx create mode 100644 assets/code/video-sdk/ensure-channel-quality/event-handler.mdx create mode 100644 assets/code/video-sdk/ensure-channel-quality/implement-call-quality-view.mdx create mode 100644 assets/code/video-sdk/ensure-channel-quality/implement-declarations.mdx rename assets/code/video-sdk/ensure-channel-quality/{swift => }/implement-labels.mdx (100%) rename assets/code/video-sdk/ensure-channel-quality/{swift => }/implement-network-status.mdx (62%) create mode 100644 assets/code/video-sdk/ensure-channel-quality/import-library.mdx create mode 100644 assets/code/video-sdk/ensure-channel-quality/probe-test.mdx create mode 100644 assets/code/video-sdk/ensure-channel-quality/set-audio-video-profile.mdx create mode 100644 assets/code/video-sdk/ensure-channel-quality/set-latency.mdx create mode 100644 assets/code/video-sdk/ensure-channel-quality/setup-engine.mdx create mode 100644 assets/code/video-sdk/ensure-channel-quality/show-stats.mdx delete mode 100644 assets/code/video-sdk/ensure-channel-quality/swift/implement-call-quality-view.mdx delete mode 100644 assets/code/video-sdk/ensure-channel-quality/swift/implement-declarations.mdx create mode 100644 assets/code/video-sdk/ensure-channel-quality/switch-quality.mdx create mode 100644 assets/code/video-sdk/ensure-channel-quality/test-hardware.mdx create mode 100644 assets/code/video-sdk/geofencing/combine-geofence.mdx create mode 100644 assets/code/video-sdk/geofencing/set-geofence.mdx create mode 100644 assets/code/video-sdk/get-started-sdk/create-engine.mdx create mode 100644 assets/code/video-sdk/get-started-sdk/declare-variables.mdx create mode 100644 assets/code/video-sdk/get-started-sdk/destroy.mdx create mode 100644 assets/code/video-sdk/get-started-sdk/handle-events.mdx create mode 100644 assets/code/video-sdk/get-started-sdk/import-library.mdx create mode 100644 assets/code/video-sdk/get-started-sdk/join-channel.mdx create mode 100644 assets/code/video-sdk/get-started-sdk/leave-channel.mdx create mode 100644 assets/code/video-sdk/get-started-sdk/local-video.mdx create mode 100644 assets/code/video-sdk/get-started-sdk/remote-video.mdx create mode 100644 assets/code/video-sdk/get-started-sdk/request-permissions.mdx create mode 100644 assets/code/video-sdk/get-started-sdk/set-user-role.mdx create mode 100644 assets/code/video-sdk/get-started-sdk/setup-audio-video-tracks.mdx delete mode 100644 assets/code/video-sdk/get-started-sdk/swift/create-ui.mdx delete mode 100644 assets/code/video-sdk/get-started-sdk/swift/handle-events.mdx delete mode 100644 assets/code/video-sdk/get-started-sdk/swift/join-and-leave.mdx delete mode 100644 assets/code/video-sdk/get-started-sdk/swift/join-channel.mdx delete mode 100644 assets/code/video-sdk/get-started-sdk/swift/leave-channel.mdx delete mode 100644 assets/code/video-sdk/get-started-sdk/swift/role-action.mdx delete mode 100644 assets/code/video-sdk/get-started-sdk/swift/show-message.mdx delete mode 100644 assets/code/video-sdk/get-started-sdk/swift/view-did-disappear.mdx create mode 100644 assets/code/video-sdk/live-streaming-multiple-channels/import-library.mdx create mode 100644 assets/code/video-sdk/live-streaming-multiple-channels/join-a-second-channel.mdx create mode 100644 assets/code/video-sdk/live-streaming-multiple-channels/leave-second-channel.mdx create mode 100644 assets/code/video-sdk/live-streaming-multiple-channels/monitor-channel-media-relay-state.mdx create mode 100644 assets/code/video-sdk/live-streaming-multiple-channels/receive-callbacks-from-second-channel.mdx create mode 100644 assets/code/video-sdk/live-streaming-multiple-channels/set-variables.mdx create mode 100644 assets/code/video-sdk/live-streaming-multiple-channels/start-stop-channel-media-relay.mdx create mode 100644 assets/code/video-sdk/play-media/configure-engine.mdx create mode 100644 assets/code/video-sdk/play-media/destroy-media-player.mdx create mode 100644 assets/code/video-sdk/play-media/display-media.mdx create mode 100644 assets/code/video-sdk/play-media/event-handler.mdx create mode 100644 assets/code/video-sdk/play-media/import-library.mdx create mode 100644 assets/code/video-sdk/play-media/play-pause-resume.mdx create mode 100644 assets/code/video-sdk/play-media/set-variables.mdx create mode 100644 assets/code/video-sdk/play-media/start-streaming.mdx create mode 100644 assets/code/video-sdk/play-media/update-channel-publish-options.mdx create mode 100644 assets/code/video-sdk/product-workflow/import-library.mdx create mode 100644 assets/code/video-sdk/product-workflow/ios-extension.mdx create mode 100644 assets/code/video-sdk/product-workflow/macos-screencapture.mdx create mode 100644 assets/code/video-sdk/product-workflow/media-device-changed.mdx create mode 100644 assets/code/video-sdk/product-workflow/microphone-camera-change.mdx create mode 100644 assets/code/video-sdk/product-workflow/mute-local-video.mdx create mode 100644 assets/code/video-sdk/product-workflow/mute-remote-user.mdx create mode 100644 assets/code/video-sdk/product-workflow/override-broadcast-started.mdx create mode 100644 assets/code/video-sdk/product-workflow/preview-screen-track.mdx create mode 100644 assets/code/video-sdk/product-workflow/publish-screen-track.mdx create mode 100644 assets/code/video-sdk/product-workflow/screen-sharer-target.mdx create mode 100644 assets/code/video-sdk/product-workflow/setup-engine.mdx create mode 100644 assets/code/video-sdk/product-workflow/setup-volume.mdx create mode 100644 assets/code/video-sdk/product-workflow/start-sharing.mdx create mode 100644 assets/code/video-sdk/product-workflow/stop-sharing.mdx create mode 100644 assets/code/video-sdk/raw-video-audio/configure-engine.mdx create mode 100644 assets/code/video-sdk/raw-video-audio/import-library.mdx create mode 100644 assets/code/video-sdk/raw-video-audio/modify-audio-video.mdx create mode 100644 assets/code/video-sdk/raw-video-audio/register-video-audio-frame-observers.mdx create mode 100644 assets/code/video-sdk/raw-video-audio/set-audio-frame-observer.mdx create mode 100644 assets/code/video-sdk/raw-video-audio/set-variables.mdx create mode 100644 assets/code/video-sdk/raw-video-audio/set-video-frame-observer.mdx create mode 100644 assets/code/video-sdk/raw-video-audio/unregister-video-audio-frame-observers.mdx create mode 100644 assets/code/video-sdk/spatial-audio/import-library.mdx create mode 100644 assets/code/video-sdk/spatial-audio/play-media.mdx create mode 100644 assets/code/video-sdk/spatial-audio/remove-spatial.mdx create mode 100644 assets/code/video-sdk/spatial-audio/set-variables.mdx create mode 100644 assets/code/video-sdk/spatial-audio/setup-local.mdx create mode 100644 assets/code/video-sdk/spatial-audio/setup-remote.mdx create mode 100644 assets/code/video-sdk/spatial-audio/setup-spatial.mdx create mode 100644 assets/code/video-sdk/virtual-background/blur-background.mdx create mode 100644 assets/code/video-sdk/virtual-background/color-background.mdx create mode 100644 assets/code/video-sdk/virtual-background/configure-engine.mdx create mode 100644 assets/code/video-sdk/virtual-background/device-compatibility.mdx create mode 100644 assets/code/video-sdk/virtual-background/image-background.mdx create mode 100644 assets/code/video-sdk/virtual-background/import-library.mdx create mode 100644 assets/code/video-sdk/virtual-background/reset-background.mdx create mode 100644 assets/code/video-sdk/virtual-background/set-virtual-background.mdx create mode 100644 assets/images/extensions-marketplace/active-fence-add-an-editable-field.png create mode 100644 assets/images/extensions-marketplace/geofencing.svg create mode 100755 assets/images/extensions-marketplace/livedata-ios-build-settings-add-objc.png create mode 100644 assets/images/extensions-marketplace/livedata-ios-build-settings-capitalize-o-and-c.png create mode 100644 assets/images/extensions-marketplace/ncs-worflow.svg create mode 100644 assets/images/flexible-classroom/ios-demo-qr.png create mode 100644 assets/images/interactive-live-streaming/live-streaming-over-multiple-channels.svg delete mode 100644 assets/images/media-gateway/media-gateway-flow.svg delete mode 100644 assets/images/media-gateway/obs-server-setting.png delete mode 100644 assets/images/media-gateway/obs-setting-bframes.png delete mode 100644 assets/images/media-gateway/obs-setting-zerolatency.png delete mode 100644 assets/images/media-gateway/overview-page-image.png delete mode 100644 assets/images/real-time-transcription/real-time-transcription.png delete mode 100644 assets/images/real-time-transcription/transcriptions.svg create mode 100644 assets/images/shared/ncs-worflow.svg create mode 100644 assets/images/signaling/configure_project.png create mode 100644 assets/images/signaling/project_settings_signaling.png create mode 100644 assets/images/signaling/signaling-pricing-plans.png delete mode 100644 assets/images/video-calling/video-composite-example.png delete mode 100644 assets/images/video-calling/video-compositor.png create mode 100644 assets/images/video-calling/video_call_workflow.svg create mode 100644 assets/images/video-calling/video_call_workflow_run_end.svg create mode 100644 assets/images/video-sdk/audio-and-voice-effects-web.puml create mode 100644 assets/images/video-sdk/audio-and-voice-effects-web.svg create mode 100644 assets/images/video-sdk/product-workflow.svg delete mode 100644 assets/images/video-sdk/real-time-transcription-server.puml delete mode 100644 assets/images/video-sdk/real-time-transcription-server.svg create mode 100644 assets/images/video-sdk/video_call_workflow.svg create mode 100644 assets/images/video-sdk/video_call_workflow_run_end.svg create mode 100644 assets/images/voice-sdk/process-raw-audio.svg create mode 100644 assets/images/voice-sdk/product-workflow-voice.svg create mode 100644 assets/images/voice-sdk/voice-call-logic-android.puml delete mode 100644 broadcast-streaming/enable-features/image-enhancement.mdx rename broadcast-streaming/{reference => overview}/pricing.mdx (94%) rename broadcast-streaming/{reference => overview}/release-notes.mdx (94%) rename broadcast-streaming/{reference => overview}/supported-platforms.mdx (100%) create mode 100644 broadcast-streaming/reference/error-codes.mdx delete mode 100644 broadcast-streaming/reference/known-issues.mdx create mode 100644 broadcast-streaming/reference/service-limits.mdx rename cloud-recording/{reference => overview}/pricing-webpage-recording.md (94%) rename cloud-recording/{reference => overview}/pricing.md (97%) rename cloud-recording/{reference => overview}/release-notes.mdx (93%) rename cloud-recording/{reference => overview}/supported-platforms.mdx (93%) create mode 100644 extensions-marketplace/develop/integrate/livedata-conversation-intelligence.mdx delete mode 100644 extensions-marketplace/develop/integrate/superclarity.mdx rename extensions-marketplace/{reference => overview}/release-notes.mdx (94%) rename extensions-marketplace/{reference => overview}/supported-platforms.mdx (99%) rename {real-time-transcription/reference => flexible-classroom/client-api}/_category_.json (70%) rename flexible-classroom/{reference => client-api}/classroom-sdk.mdx (100%) rename flexible-classroom/{reference => client-api}/edu-context-sdk.mdx (100%) rename flexible-classroom/{reference => client-api}/proctor-sdk.mdx (100%) rename flexible-classroom/{reference => client-api}/ui-scene.mdx (91%) rename flexible-classroom/{reference => get-started}/manage-agora-account.mdx (94%) rename flexible-classroom/{reference => overview}/downloads.mdx (85%) rename flexible-classroom/{reference => overview}/pricing.md (96%) rename flexible-classroom/{reference => overview}/release-notes.mdx (94%) rename flexible-classroom/{reference => overview}/supported-platforms.md (100%) rename flexible-classroom/{reference => overview}/technical-architecture.md (100%) create mode 100644 flexible-classroom/restful-api/_category_.json rename flexible-classroom/{reference => restful-api}/classroom-api.mdx (91%) rename interactive-live-streaming/{reference => overview}/pricing.mdx (95%) rename interactive-live-streaming/{reference => overview}/release-notes.mdx (94%) rename interactive-live-streaming/{reference => overview}/supported-platforms.mdx (100%) create mode 100644 interactive-live-streaming/reference/error-codes.mdx delete mode 100644 interactive-live-streaming/reference/known-issues.mdx create mode 100644 interactive-live-streaming/reference/service-limits.mdx rename interactive-whiteboard/{reference => overview}/pricing.md (95%) rename interactive-whiteboard/{reference => overview}/release-notes-uikit.mdx (94%) rename interactive-whiteboard/{reference => overview}/release-notes.mdx (94%) rename interactive-whiteboard/{reference => overview}/supported-platforms.mdx (93%) rename iot/{reference => overview}/pricing.mdx (94%) rename iot/{reference => overview}/release-notes.mdx (93%) rename iot/{reference => overview}/supported-platforms.mdx (100%) delete mode 100644 media-gateway/develop/_category_.json delete mode 100644 media-gateway/get-started/_category_.json delete mode 100644 media-gateway/get-started/quickstart.mdx delete mode 100644 media-gateway/overview/_category_.json delete mode 100644 media-gateway/overview/core-concepts.mdx delete mode 100644 media-gateway/overview/product-overview.mdx delete mode 100644 media-gateway/reference/_category_.json delete mode 100644 media-gateway/reference/best-practice.mdx delete mode 100644 media-gateway/reference/glossary.mdx delete mode 100644 media-gateway/reference/manage-agora-account.mdx delete mode 100644 media-gateway/reference/release-notes.mdx delete mode 100644 media-gateway/reference/security.mdx rename media-pull/{reference => overview}/pricing.mdx (94%) rename media-pull/{reference => overview}/release-notes.mdx (100%) rename media-push/{reference => overview}/pricing.mdx (94%) rename on-premise-recording/{reference => overview}/billing.md (99%) rename on-premise-recording/{reference => overview}/release-notes.mdx (99%) delete mode 100644 real-time-transcription/get-started/_category_.json delete mode 100644 real-time-transcription/get-started/get-started.mdx delete mode 100644 real-time-transcription/overview/_category_.json delete mode 100644 real-time-transcription/overview/core-concepts.mdx delete mode 100644 real-time-transcription/overview/product-overview.mdx delete mode 100644 real-time-transcription/reference/glossary.mdx delete mode 100644 real-time-transcription/reference/manage-agora-account.mdx delete mode 100644 real-time-transcription/reference/pricing.mdx delete mode 100644 real-time-transcription/reference/security.mdx rename server-gateway/{reference => overview}/pricing.mdx (93%) rename server-gateway/{reference => overview}/release-notes.mdx (94%) create mode 100644 shared/chat-sdk/get-started/get-started-uikit/prerequisites/flutter.mdx create mode 100644 shared/chat-sdk/get-started/get-started-uikit/prerequisites/react-native.mdx create mode 100644 shared/chat-sdk/get-started/get-started-uikit/project-implementation/flutter.mdx create mode 100644 shared/chat-sdk/get-started/get-started-uikit/project-implementation/react-native.mdx create mode 100644 shared/chat-sdk/get-started/get-started-uikit/project-setup/flutter.mdx create mode 100644 shared/chat-sdk/get-started/get-started-uikit/project-test/flutter.mdx create mode 100644 shared/chat-sdk/get-started/get-started-uikit/reference/flutter.mdx create mode 100644 shared/common/project-setup/linux-cpp.mdx create mode 100644 shared/common/project-setup/new-project/android.mdx create mode 100644 shared/common/project-setup/new-project/blueprint.mdx create mode 100644 shared/common/project-setup/new-project/electron.mdx rename shared/{video-sdk/get-started/get-started-sdk/project-setup => common/project-setup/new-project}/flutter.mdx (100%) create mode 100644 shared/common/project-setup/new-project/index.mdx rename shared/{video-sdk/develop/spatial-audio/project-implementation => common/project-setup/new-project}/ios.mdx (81%) rename shared/{video-sdk/develop/real-time-transcription/project-test => common/project-setup/new-project}/macos.mdx (100%) create mode 100644 shared/common/project-setup/new-project/react-js.mdx create mode 100644 shared/common/project-setup/new-project/react-native.mdx create mode 100644 shared/common/project-setup/new-project/swift.mdx create mode 100644 shared/common/project-setup/new-project/unity.mdx create mode 100644 shared/common/project-setup/new-project/unreal.mdx create mode 100644 shared/common/project-setup/new-project/web.mdx create mode 100644 shared/common/project-setup/new-project/windows.mdx create mode 100644 shared/common/project-test/clone-project.mdx create mode 100644 shared/common/project-test/generate-temp-rtc-token.mdx create mode 100644 shared/common/project-test/load-web-demo.mdx create mode 100644 shared/common/project-test/rtc-first-steps.mdx create mode 100644 shared/common/project-test/set-app-id.mdx create mode 100644 shared/common/project-test/set-authentication-rtc.mdx delete mode 100644 shared/extensions-marketplace/_superclarity.mdx create mode 100644 shared/extensions-marketplace/ai-noise-suppression/project-implementation/poc3.mdx create mode 100644 shared/extensions-marketplace/ai-noise-suppression/project-test/poc3.mdx create mode 100644 shared/extensions-marketplace/ai-noise-suppression/project-test/react-js.mdx create mode 100644 shared/extensions-marketplace/common/project-test/poc3.mdx delete mode 100644 shared/extensions-marketplace/integrations/bose-pinpoint.mdx create mode 100644 shared/extensions-marketplace/livedata-conversation-intelligence/index.mdx create mode 100644 shared/extensions-marketplace/livedata-conversation-intelligence/project-implementation/android.mdx rename shared/{interactive-whiteboard/fastboard-api => extensions-marketplace/livedata-conversation-intelligence/project-implementation}/index.mdx (69%) create mode 100644 shared/extensions-marketplace/livedata-conversation-intelligence/project-implementation/ios.mdx create mode 100644 shared/extensions-marketplace/livedata-conversation-intelligence/project-setup/android.mdx create mode 100644 shared/extensions-marketplace/livedata-conversation-intelligence/project-setup/index.mdx create mode 100644 shared/extensions-marketplace/livedata-conversation-intelligence/project-setup/ios.mdx create mode 100644 shared/extensions-marketplace/livedata-conversation-intelligence/reference/android.mdx create mode 100644 shared/extensions-marketplace/livedata-conversation-intelligence/reference/index.mdx create mode 100644 shared/extensions-marketplace/livedata-conversation-intelligence/reference/ios.mdx create mode 100644 shared/extensions-marketplace/virtual-background/project-implementation/poc3.mdx create mode 100644 shared/extensions-marketplace/virtual-background/project-test/poc3.mdx create mode 100644 shared/extensions-marketplace/virtual-background/project-test/react-js.mdx delete mode 100644 shared/extensions-marketplace/virtual-background/project-test/swift.mdx create mode 100644 shared/flexible-classroom/customize-ui-scene-sdk/android.mdx create mode 100644 shared/flexible-classroom/customize-ui-scene-sdk/ios.mdx delete mode 100644 shared/interactive-whiteboard/fastboard-api/android.mdx delete mode 100644 shared/interactive-whiteboard/fastboard-api/ios.mdx delete mode 100644 shared/interactive-whiteboard/fastboard-api/web.mdx delete mode 100644 shared/media-gateway/get-started/_quickstart.mdx delete mode 100644 shared/media-gateway/reference/_best-practice.mdx delete mode 100644 shared/media-gateway/reference/_release-notes.mdx create mode 100644 shared/notification-center-service/release-notes.mdx create mode 100644 shared/signaling/authentication-workflow/implementation/android.mdx create mode 100644 shared/signaling/authentication-workflow/implementation/index.mdx create mode 100644 shared/signaling/authentication-workflow/implementation/ios.mdx rename shared/{video-sdk/develop/real-time-transcription/project-setup => signaling/authentication-workflow/implementation}/react-native.mdx (100%) create mode 100644 shared/signaling/authentication-workflow/implementation/unity.mdx create mode 100644 shared/signaling/authentication-workflow/implementation/web.mdx create mode 100644 shared/signaling/authentication-workflow/implementation/windows.mdx create mode 100644 shared/signaling/authentication-workflow/test/android.mdx rename shared/{video-sdk/develop/real-time-transcription/project-setup => signaling/authentication-workflow/test}/index.mdx (61%) create mode 100644 shared/signaling/authentication-workflow/test/ios.mdx rename shared/{video-sdk/develop/real-time-transcription/reference => signaling/authentication-workflow/test}/react-native.mdx (100%) create mode 100644 shared/signaling/authentication-workflow/test/unity.mdx create mode 100644 shared/signaling/authentication-workflow/test/web.mdx create mode 100644 shared/signaling/authentication-workflow/test/windows.mdx create mode 100644 shared/signaling/beginners-guide/index.mdx create mode 100644 shared/signaling/cloud-proxy/_cloud-proxy.mdx create mode 100644 shared/signaling/cloud-proxy/project-implementation/android.mdx create mode 100644 shared/signaling/cloud-proxy/project-implementation/electron.mdx create mode 100644 shared/signaling/cloud-proxy/project-implementation/flutter.mdx rename shared/{video-sdk/develop/real-time-transcription => signaling/cloud-proxy}/project-implementation/index.mdx (69%) create mode 100644 shared/signaling/cloud-proxy/project-implementation/ios.mdx create mode 100644 shared/signaling/cloud-proxy/project-implementation/macos.mdx create mode 100644 shared/signaling/cloud-proxy/project-implementation/react-native.mdx create mode 100644 shared/signaling/cloud-proxy/project-implementation/swift.mdx create mode 100644 shared/signaling/cloud-proxy/project-implementation/unity.mdx create mode 100644 shared/signaling/cloud-proxy/project-implementation/web.mdx create mode 100644 shared/signaling/cloud-proxy/project-implementation/windows.mdx create mode 100644 shared/signaling/cloud-proxy/project-test/android.mdx rename shared/{video-sdk/develop/video-compositor/project-test/web.mdx => signaling/cloud-proxy/project-test/electron.mdx} (64%) create mode 100644 shared/signaling/cloud-proxy/project-test/flutter.mdx rename shared/{video-sdk/develop => signaling}/cloud-proxy/project-test/ios.mdx (100%) rename shared/{video-sdk/develop => signaling}/cloud-proxy/project-test/macos.mdx (100%) create mode 100644 shared/signaling/cloud-proxy/project-test/react-native.mdx create mode 100644 shared/signaling/cloud-proxy/project-test/unity.mdx create mode 100644 shared/signaling/cloud-proxy/project-test/web.mdx create mode 100644 shared/signaling/cloud-proxy/project-test/windows.mdx create mode 100644 shared/signaling/data-encryption/_data_encryption.mdx create mode 100644 shared/signaling/data-encryption/project-implementation/android.mdx create mode 100644 shared/signaling/data-encryption/project-implementation/electron.mdx create mode 100644 shared/signaling/data-encryption/project-implementation/flutter.mdx rename shared/{video-sdk/get-started/get-started-sdk/project-setup => signaling/data-encryption/project-implementation}/index.mdx (87%) rename shared/{video-sdk/get-started/get-started-sdk => signaling/data-encryption}/project-implementation/ios.mdx (99%) rename shared/{video-sdk/get-started/get-started-sdk => signaling/data-encryption}/project-implementation/macos.mdx (100%) create mode 100644 shared/signaling/data-encryption/project-implementation/react-native.mdx create mode 100644 shared/signaling/data-encryption/project-implementation/swift.mdx create mode 100644 shared/signaling/data-encryption/project-implementation/unity.mdx create mode 100644 shared/signaling/data-encryption/project-implementation/web.mdx create mode 100644 shared/signaling/data-encryption/project-implementation/windows.mdx create mode 100644 shared/signaling/data-encryption/project-test/android.mdx create mode 100644 shared/signaling/data-encryption/project-test/electron.mdx create mode 100644 shared/signaling/data-encryption/project-test/flutter.mdx rename shared/{extensions-marketplace/virtual-background => signaling/data-encryption}/project-test/ios.mdx (81%) rename shared/{extensions-marketplace/virtual-background => signaling/data-encryption}/project-test/macos.mdx (81%) create mode 100644 shared/signaling/data-encryption/project-test/react-native.mdx create mode 100644 shared/signaling/data-encryption/project-test/swift.mdx create mode 100644 shared/signaling/data-encryption/project-test/unity.mdx rename shared/{video-sdk/develop/real-time-transcription/reference => signaling/data-encryption/project-test}/web.mdx (98%) create mode 100644 shared/signaling/data-encryption/project-test/windows.mdx create mode 100644 shared/signaling/geofencing/project-test/android.mdx create mode 100644 shared/signaling/geofencing/project-test/electron.mdx create mode 100644 shared/signaling/geofencing/project-test/flutter.mdx create mode 100644 shared/signaling/geofencing/project-test/ios.mdx create mode 100644 shared/signaling/geofencing/project-test/macos.mdx create mode 100644 shared/signaling/geofencing/project-test/react-native.mdx create mode 100644 shared/signaling/geofencing/project-test/unity.mdx rename shared/{video-sdk/develop/real-time-transcription/project-setup => signaling/geofencing/project-test}/web.mdx (94%) create mode 100644 shared/signaling/geofencing/project-test/windows.mdx create mode 100644 shared/signaling/presence/project-implementation/index.mdx create mode 100644 shared/signaling/presence/project-implementation/web.mdx create mode 100644 shared/signaling/presence/project-test/web.mdx create mode 100644 shared/signaling/reference/api-ref/linux-cpp.mdx create mode 100644 shared/signaling/reference/api-ref/linux-cpp/_channel-en.cpp.mdx create mode 100644 shared/signaling/reference/api-ref/linux-cpp/_configuration-en.cpp.mdx create mode 100644 shared/signaling/reference/api-ref/linux-cpp/_enumv-en.mdx create mode 100644 shared/signaling/reference/api-ref/linux-cpp/_lock-en.cpp.mdx create mode 100644 shared/signaling/reference/api-ref/linux-cpp/_message-en.cpp.mdx create mode 100644 shared/signaling/reference/api-ref/linux-cpp/_presence-en.cpp.mdx create mode 100644 shared/signaling/reference/api-ref/linux-cpp/_storage-en.cpp.mdx create mode 100644 shared/signaling/reference/api-ref/linux-cpp/_topic-en.cpp.mdx create mode 100644 shared/signaling/reference/api-ref/linux-cpp/troubleshooting.mdx create mode 100644 shared/signaling/reference/api-ref/troubleshooting.mdx create mode 100644 shared/signaling/reference/api-ref/unity.mdx create mode 100644 shared/signaling/reference/api-ref/unity/_channel-en.unity.mdx create mode 100644 shared/signaling/reference/api-ref/unity/_configuration-en.unity.mdx create mode 100644 shared/signaling/reference/api-ref/unity/_enumv-en.mdx create mode 100644 shared/signaling/reference/api-ref/unity/_lock-en.unity.mdx create mode 100644 shared/signaling/reference/api-ref/unity/_message-en.unity.mdx create mode 100644 shared/signaling/reference/api-ref/unity/_presence-en.unity.mdx create mode 100644 shared/signaling/reference/api-ref/unity/_storage-en.unity.mdx create mode 100644 shared/signaling/reference/api-ref/unity/_topic-en.unity.mdx create mode 100644 shared/signaling/reference/api-ref/unity/troubleshooting.mdx create mode 100644 shared/signaling/release-notes/linux-cpp.mdx create mode 100644 shared/signaling/release-notes/unity.mdx create mode 100644 shared/signaling/storage/project-implementation/android.mdx create mode 100644 shared/signaling/storage/project-implementation/index.mdx rename shared/{video-sdk/develop/real-time-transcription/project-test => signaling/storage/project-implementation}/ios.mdx (81%) create mode 100644 shared/signaling/storage/project-implementation/macos.mdx create mode 100644 shared/signaling/storage/project-implementation/swift.mdx create mode 100644 shared/signaling/storage/project-implementation/web.mdx rename shared/{video-sdk/develop/real-time-transcription/project-setup => signaling/storage/project-test}/android.mdx (100%) rename shared/{video-sdk/develop/real-time-transcription/project-setup => signaling/storage/project-test}/electron.mdx (100%) rename shared/{video-sdk/develop/real-time-transcription/project-setup => signaling/storage/project-test}/flutter.mdx (100%) rename shared/{video-sdk/develop/real-time-transcription/project-setup => signaling/storage/project-test}/ios.mdx (100%) create mode 100644 shared/signaling/storage/project-test/linux-cpp.mdx rename shared/{video-sdk/develop/real-time-transcription/project-setup => signaling/storage/project-test}/macos.mdx (65%) create mode 100644 shared/signaling/storage/project-test/react-native.mdx rename shared/{video-sdk/develop/real-time-transcription/project-setup => signaling/storage/project-test}/swift.mdx (100%) create mode 100644 shared/signaling/storage/project-test/unity.mdx create mode 100644 shared/signaling/storage/project-test/web.mdx create mode 100644 shared/signaling/storage/project-test/windows.mdx create mode 100644 shared/signaling/stream-channel/_stream-channel.mdx create mode 100644 shared/signaling/stream-channel/project-implementation/android.mdx create mode 100644 shared/signaling/stream-channel/project-implementation/electron.mdx create mode 100644 shared/signaling/stream-channel/project-implementation/flutter.mdx rename shared/{video-sdk/develop/real-time-transcription/project-test => signaling/stream-channel/project-implementation}/index.mdx (69%) create mode 100644 shared/signaling/stream-channel/project-implementation/ios.mdx create mode 100644 shared/signaling/stream-channel/project-implementation/macos.mdx create mode 100644 shared/signaling/stream-channel/project-implementation/react-native.mdx create mode 100644 shared/signaling/stream-channel/project-implementation/swift.mdx create mode 100644 shared/signaling/stream-channel/project-implementation/unity.mdx create mode 100644 shared/signaling/stream-channel/project-implementation/web.mdx create mode 100644 shared/signaling/stream-channel/project-implementation/windows.mdx create mode 100644 shared/signaling/stream-channel/project-test/android.mdx create mode 100644 shared/signaling/stream-channel/project-test/electron.mdx create mode 100644 shared/signaling/stream-channel/project-test/flutter.mdx create mode 100644 shared/signaling/stream-channel/project-test/ios.mdx create mode 100644 shared/signaling/stream-channel/project-test/macos.mdx create mode 100644 shared/signaling/stream-channel/project-test/react-native.mdx create mode 100644 shared/signaling/stream-channel/project-test/unity.mdx create mode 100644 shared/signaling/stream-channel/project-test/web.mdx create mode 100644 shared/signaling/stream-channel/project-test/windows.mdx create mode 100644 shared/video-sdk/authentication-workflow/project-implementation/poc3.mdx delete mode 100644 shared/video-sdk/authentication-workflow/project-implementation/web.mdx create mode 100644 shared/video-sdk/authentication-workflow/project-test/poc3.mdx delete mode 100644 shared/video-sdk/develop/_real-time-transcription.mdx delete mode 100644 shared/video-sdk/develop/_video-compositor.mdx create mode 100644 shared/video-sdk/develop/audio-and-voice-effects/project-implementation/poc3.mdx create mode 100644 shared/video-sdk/develop/audio-and-voice-effects/project-test/poc3.mdx create mode 100644 shared/video-sdk/develop/audio-and-voice-effects/project-test/react-js.mdx create mode 100644 shared/video-sdk/develop/cloud-proxy/project-implementation/poc3.mdx delete mode 100644 shared/video-sdk/develop/cloud-proxy/project-implementation/web.mdx rename shared/video-sdk/develop/{real-time-transcription => cloud-proxy}/project-setup/unity.mdx (100%) create mode 100644 shared/video-sdk/develop/cloud-proxy/project-test/poc3.mdx create mode 100644 shared/video-sdk/develop/cloud-proxy/project-test/react-js.mdx create mode 100644 shared/video-sdk/develop/custom-video-and-audio/project-implementation/poc3.mdx delete mode 100644 shared/video-sdk/develop/custom-video-and-audio/project-implementation/web.mdx create mode 100644 shared/video-sdk/develop/custom-video-and-audio/project-test/poc3.mdx create mode 100644 shared/video-sdk/develop/custom-video-and-audio/project-test/react-js.mdx create mode 100644 shared/video-sdk/develop/encrypt-media-streams/project-implementation/poc3.mdx delete mode 100644 shared/video-sdk/develop/encrypt-media-streams/project-implementation/web.mdx create mode 100644 shared/video-sdk/develop/encrypt-media-streams/project-test/poc3.mdx create mode 100644 shared/video-sdk/develop/encrypt-media-streams/project-test/react-js.mdx create mode 100644 shared/video-sdk/develop/ensure-channel-quality/project-implementation/poc3.mdx delete mode 100644 shared/video-sdk/develop/ensure-channel-quality/project-implementation/web.mdx create mode 100644 shared/video-sdk/develop/ensure-channel-quality/project-test/poc3.mdx create mode 100644 shared/video-sdk/develop/ensure-channel-quality/project-test/react-js.mdx create mode 100644 shared/video-sdk/develop/geofencing/project-implementation/poc3.mdx delete mode 100644 shared/video-sdk/develop/geofencing/project-implementation/web.mdx create mode 100644 shared/video-sdk/develop/geofencing/project-test/poc3.mdx create mode 100644 shared/video-sdk/develop/geofencing/project-test/react-js.mdx create mode 100644 shared/video-sdk/develop/live-streaming-over-multiple-channels/project-implementation/poc3.mdx delete mode 100644 shared/video-sdk/develop/live-streaming-over-multiple-channels/project-implementation/web.mdx create mode 100644 shared/video-sdk/develop/live-streaming-over-multiple-channels/project-test/poc3.mdx create mode 100644 shared/video-sdk/develop/live-streaming-over-multiple-channels/project-test/react-js.mdx delete mode 100644 shared/video-sdk/develop/live-streaming-over-multiple-channels/project-test/swift.mdx create mode 100644 shared/video-sdk/develop/play-media/project-implementation/poc3.mdx delete mode 100644 shared/video-sdk/develop/play-media/project-implementation/web.mdx create mode 100644 shared/video-sdk/develop/play-media/project-test/poc3.mdx create mode 100644 shared/video-sdk/develop/play-media/project-test/react-js.mdx create mode 100644 shared/video-sdk/develop/product-workflow/project-implementation/poc3.mdx delete mode 100644 shared/video-sdk/develop/product-workflow/project-implementation/web.mdx create mode 100644 shared/video-sdk/develop/product-workflow/project-test/poc3.mdx create mode 100644 shared/video-sdk/develop/product-workflow/project-test/react-js.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/project-implementation/android.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/project-implementation/electron.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/project-implementation/flutter.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/project-implementation/ios.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/project-implementation/macos.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/project-implementation/react-native.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/project-implementation/unity.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/project-implementation/web.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/project-implementation/windows.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/project-test/android.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/project-test/electron.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/project-test/flutter.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/project-test/react-native.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/project-test/swift.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/project-test/unity.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/project-test/web.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/project-test/windows.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/reference/android.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/reference/electron.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/reference/flutter.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/reference/index.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/reference/ios.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/reference/macos.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/reference/swift.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/reference/unity.mdx delete mode 100644 shared/video-sdk/develop/real-time-transcription/reference/windows.mdx create mode 100644 shared/video-sdk/develop/spatial-audio/project-implementation/poc3.mdx delete mode 100644 shared/video-sdk/develop/spatial-audio/project-implementation/swift.mdx delete mode 100644 shared/video-sdk/develop/spatial-audio/project-implementation/web.mdx create mode 100644 shared/video-sdk/develop/spatial-audio/project-test/poc3.mdx create mode 100644 shared/video-sdk/develop/spatial-audio/project-test/react-js.mdx create mode 100644 shared/video-sdk/develop/stream-raw-audio-and-video/project-implementation/poc3.mdx create mode 100644 shared/video-sdk/develop/stream-raw-audio-and-video/project-test/poc3.mdx delete mode 100644 shared/video-sdk/develop/video-compositor/project-implementation/index.mdx delete mode 100644 shared/video-sdk/develop/video-compositor/project-implementation/web.mdx delete mode 100644 shared/video-sdk/develop/video-compositor/project-setup/index.mdx delete mode 100644 shared/video-sdk/develop/video-compositor/project-setup/web.mdx delete mode 100644 shared/video-sdk/develop/video-compositor/project-test/index.mdx delete mode 100644 shared/video-sdk/develop/video-compositor/reference/index.mdx delete mode 100644 shared/video-sdk/develop/video-compositor/reference/web.mdx delete mode 100644 shared/video-sdk/get-started/get-started-sdk/project-implementation/android.mdx delete mode 100644 shared/video-sdk/get-started/get-started-sdk/project-implementation/flutter.mdx create mode 100644 shared/video-sdk/get-started/get-started-sdk/project-implementation/poc3.mdx delete mode 100644 shared/video-sdk/get-started/get-started-sdk/project-implementation/swift.mdx delete mode 100644 shared/video-sdk/get-started/get-started-sdk/project-implementation/web.mdx create mode 100644 shared/video-sdk/get-started/get-started-sdk/project-setup/react-js.mdx create mode 100644 shared/video-sdk/get-started/get-started-sdk/project-test/poc3.mdx create mode 100644 shared/video-sdk/reference/_error-codes.mdx delete mode 100644 shared/video-sdk/reference/_known-issues.mdx create mode 100644 shared/video-sdk/reference/_service_limits.mdx delete mode 100644 shared/video-sdk/reference/app-size-optimization/project-implementation/unreal.mdx rename shared/voice-sdk/{_authentication-workflow.mdx => authentication-workflow/index.mdx} (98%) delete mode 100644 shared/voice-sdk/develop/audio-and-voice-effects/reference/unreal.mdx rename shared/voice-sdk/develop/{_ensure-channel-quality.mdx => ensure-channel-quality/index.mdx} (93%) create mode 100644 shared/voice-sdk/develop/ensure-channel-quality/project-implementation/poc3.mdx create mode 100644 shared/voice-sdk/develop/ensure-channel-quality/project-test/.web.mdx create mode 100644 shared/voice-sdk/develop/ensure-channel-quality/project-test/poc3.mdx create mode 100644 shared/voice-sdk/develop/ensure-channel-quality/project-test/react-js.mdx create mode 100644 shared/voice-sdk/develop/ensure-channel-quality/project-test/windows.mdx rename shared/voice-sdk/develop/{_product-workflow.mdx => product-workflow/index.mdx} (89%) create mode 100644 shared/voice-sdk/develop/product-workflow/project-implementation/poc3.mdx delete mode 100644 shared/voice-sdk/develop/product-workflow/project-implementation/web.mdx create mode 100644 shared/voice-sdk/develop/stream-raw-audio/project-implementation/poc3.mdx rename shared/voice-sdk/get-started/{_get-started-sdk.mdx => get-started-sdk/index.mdx} (61%) create mode 100644 shared/voice-sdk/get-started/get-started-sdk/project-implementation/poc3.mdx delete mode 100644 shared/voice-sdk/reference/_known-issues.mdx create mode 100644 shared/voice-sdk/reference/_service_limits.mdx create mode 100644 shared/voice-sdk/reference/release-notes/react-js.mdx rename signaling/{get-started => develop}/get-started-sdk.mdx (100%) create mode 100644 signaling/develop/locks.mdx rename signaling/{get-started => develop}/stream-channel.mdx (100%) create mode 100644 signaling/develop/topics.mdx create mode 100644 signaling/get-started/beginners-guide.mdx rename signaling/{develop => get-started}/integrate-token-generation.mdx (94%) create mode 100644 signaling/get-started/migration-guide.mdx create mode 100644 signaling/overview/pricing.mdx rename signaling/{reference => overview}/release-notes.mdx (96%) rename signaling/{reference => overview}/supported-platforms.mdx (100%) delete mode 100644 signaling/reference/pricing.mdx create mode 100644 signaling/reference/restful-messaging.mdx create mode 100644 signaling/reference/user-channel-events.mdx delete mode 100644 video-calling/enable-features/image-enhancement.mdx delete mode 100644 video-calling/enable-features/video-compositor.mdx rename video-calling/{reference => overview}/pricing.mdx (94%) rename video-calling/{reference => overview}/release-notes.mdx (93%) rename video-calling/{reference => overview}/supported-platforms.mdx (100%) create mode 100644 video-calling/reference/error-codes.mdx delete mode 100644 video-calling/reference/known-issues.mdx create mode 100644 video-calling/reference/service-limits.mdx rename voice-calling/{reference => overview}/pricing.mdx (94%) rename voice-calling/{reference => overview}/release-notes.mdx (93%) rename voice-calling/{reference => overview}/supported-platforms.mdx (93%) create mode 100644 voice-calling/reference/error-codes.mdx delete mode 100644 voice-calling/reference/known-issues.mdx create mode 100644 voice-calling/reference/service-limits.mdx diff --git a/.github/way-of-working.md b/.github/way-of-working.md index 9de9a8118..60ad39173 100644 --- a/.github/way-of-working.md +++ b/.github/way-of-working.md @@ -3,7 +3,7 @@ Documentation is a crucial part of Agora's offering to customers. This document describes how the EN doc team plans and carries out documentation activities to make sure that everything on Agora's -[documentation website](docs.agora.io) is up to standard and released in a timely manner. +[documentation website](docs.agora.io) is up to standard and released in a timely manner. ## EN doc team structure diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..496ee2ca6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/agora-analytics/reference/pricing.mdx b/agora-analytics/overview/pricing.mdx similarity index 94% rename from agora-analytics/reference/pricing.mdx rename to agora-analytics/overview/pricing.mdx index fdf3655ac..eb7fa58d0 100644 --- a/agora-analytics/reference/pricing.mdx +++ b/agora-analytics/overview/pricing.mdx @@ -1,6 +1,6 @@ --- title: 'Pricing' -sidebar_position: 1 +sidebar_position: 3 platform_selector: false description: > Provides you with information on billing, fee deductions, free-of-charge policy, and any suspension to your account based on the account type. diff --git a/agora-analytics/overview/product-overview.mdx b/agora-analytics/overview/product-overview.mdx index 4478d93f1..372419bbd 100644 --- a/agora-analytics/overview/product-overview.mdx +++ b/agora-analytics/overview/product-overview.mdx @@ -10,7 +10,7 @@ description: > diff --git a/agora-analytics/reference/supported-platforms.mdx b/agora-analytics/overview/supported-platforms.mdx similarity index 78% rename from agora-analytics/reference/supported-platforms.mdx rename to agora-analytics/overview/supported-platforms.mdx index 8aaa981a3..625f0fe41 100644 --- a/agora-analytics/reference/supported-platforms.mdx +++ b/agora-analytics/overview/supported-platforms.mdx @@ -1,10 +1,10 @@ --- title: 'Supported platforms' -sidebar_position: 6 +sidebar_position: 5 type: docs platform_selector: false description: > - A list of terms used in Agora documentation. + A list of platforms supported by Agora Analytics. --- import SupportedPlatform from '@docs/shared/common/_supported-platforms.mdx'; diff --git a/agora-chat/get-started/get-started-uikit.mdx b/agora-chat/get-started/get-started-uikit.mdx index a5bd4fc79..d641b83be 100644 --- a/agora-chat/get-started/get-started-uikit.mdx +++ b/agora-chat/get-started/get-started-uikit.mdx @@ -1,5 +1,5 @@ --- -title: 'UI Samples quickstart' +title: 'UI Kit quickstart' sidebar_position: 2 type: docs description: > diff --git a/agora-chat/reference/pricing.mdx b/agora-chat/overview/pricing.mdx similarity index 100% rename from agora-chat/reference/pricing.mdx rename to agora-chat/overview/pricing.mdx diff --git a/agora-chat/reference/release-notes.mdx b/agora-chat/overview/release-notes.mdx similarity index 93% rename from agora-chat/reference/release-notes.mdx rename to agora-chat/overview/release-notes.mdx index 5c3af1c7b..c05ffe6c0 100644 --- a/agora-chat/reference/release-notes.mdx +++ b/agora-chat/overview/release-notes.mdx @@ -1,6 +1,6 @@ --- title: 'Release notes' -sidebar_position: 1 +sidebar_position: 4 type: docs description: > Provides release notes of Agora Chat. diff --git a/agora-chat/reference/supported-platforms.mdx b/agora-chat/overview/supported-platforms.mdx similarity index 93% rename from agora-chat/reference/supported-platforms.mdx rename to agora-chat/overview/supported-platforms.mdx index 76197536b..bb3522782 100644 --- a/agora-chat/reference/supported-platforms.mdx +++ b/agora-chat/overview/supported-platforms.mdx @@ -1,6 +1,6 @@ --- title: 'Supported platforms' -sidebar_position: 2 +sidebar_position: 5 type: docs description: > Lists the platform that Chat supports. diff --git a/assets/code/signaling/authentication-workflow/handle-expire-event.mdx b/assets/code/signaling/authentication-workflow/handle-expire-event.mdx index d67f537bc..93412f3bc 100644 --- a/assets/code/signaling/authentication-workflow/handle-expire-event.mdx +++ b/assets/code/signaling/authentication-workflow/handle-expire-event.mdx @@ -45,3 +45,51 @@ A token expires after the `tokenExpiryTime` specified in the call to the token s + + + ``` csharp + // Handle token expiration event + public async override void OnTokenPrivilegeWillExpire(string channelName) + { + // Log an informational message + Debug.Log($"OnTokenPrivilegeWillExpire channelName {channelName}"); + + // Asynchronously fetch a new token + await FetchToken(); + + // Check if a valid token is retrieved + if (!string.IsNullOrEmpty(configData.token)) + { + // Asynchronously renew the RTM token + var result = await signalingEngine.RenewTokenAsync(configData.token); + + // Log an error if the token renewal fails + if (result.Status.Error) + { + Debug.LogError($"Failed to renew token. Error: {result.Status.Reason}"); + } + } + else + { + // Log an error if the token was not retrieved + Debug.LogError("Token was not retrieved"); + } + } + ``` + + * Event Listeners + + + + ```cpp + void AuthenticationEventHandler::onTokenPrivilegeWillExpire(const char *channelName) + { + int ret = signalingManagerAuthentication->renewToken(); + if (ret == 0) + { + std::cout << "Token renewed\n"; + } + } + ``` + * Event Listeners + diff --git a/assets/code/signaling/authentication-workflow/join-stream-channel.mdx b/assets/code/signaling/authentication-workflow/join-stream-channel.mdx index c3949029d..56d0cffe8 100644 --- a/assets/code/signaling/authentication-workflow/join-stream-channel.mdx +++ b/assets/code/signaling/authentication-workflow/join-stream-channel.mdx @@ -96,4 +96,101 @@ Create a stream channel using the channel name and call `join` with the RTC toke * join + + ```csharp + public async void JoinAndLeaveStreamChannel(string channelName) + { + if(!isStreamChannelJoined) + { + if (signalingChannel == null) + { + CreateChannel(channelName); + } + + // Fetch a rtc token for the stream channel + await FetchRtcToken(channelName, configData.uid); + + if (configData.rtcToken == "") + { + LogInfo("Token was not fetched from the server"); + return; + } + + // Configure the channel options + JoinChannelOptions options = new JoinChannelOptions(); + options.token = configData.rtcToken; + options.withMetadata = false; + options.withPresence = true; + options.withLock = false; + + // Join the stream channel + var result = await signalingChannel.JoinAsync(options); + if (result.Status.Error) + { + LogError(string.Format("Join Status.Reason:{0} ", result.Status.Reason)); + } + else + { + string str = string.Format("Join Response: channelName:{0} userId:{1}", + result.Response.ChannelName, result.Response.UserId); + isStreamChannelJoined = true; + LogInfo(str); + } + } + else + { + var result = await signalingChannel.LeaveAsync(); + if (result.Status.Error) + { + LogError(string.Format("StreamChannel.Leave Status.ErrorCode:{0} ", result.Status.ErrorCode)); + } + else + { + string str = string.Format("StreamChannel.Leave Response: channelName:{0} userId:{1}", + result.Response.ChannelName, result.Response.UserId); + isStreamChannelJoined = false; + LogInfo(str); + } + + } + + } + ``` + * JoinAsync + * LeaveAsync + + + + + ```cpp + void SignalingManagerStreamChannel::joinStreamChannel(std::string channelName) + { + // Create a stream channel + channelType = RTM_CHANNEL_TYPE_STREAM; + streamChannel = signalingEngine->createStreamChannel(channelName.c_str()); + if (streamChannel == nullptr) + { + printf("createStreamChannel failed\n"); + return; + } + else + { + printf("createStreamChannel success\n"); + this->channelName = channelName; + } + + // Join the stream channel + std::string rtcToken = fetchRTCToken(channelName); + JoinChannelOptions options; + options.token = rtcToken.c_str(); + options.withLock = true; + options.withMetadata = true; + options.withPresence = true; + + uint64_t requestId; // Output parameter used to identify and process the result + int ret = streamChannel->join(options, requestId); + std::cout << "joinStreamChannel returned: " << ret << std::endl; + } + ``` + \ No newline at end of file diff --git a/assets/code/signaling/authentication-workflow/login.mdx b/assets/code/signaling/authentication-workflow/login.mdx index 9b940a4ed..ef8873a76 100644 --- a/assets/code/signaling/authentication-workflow/login.mdx +++ b/assets/code/signaling/authentication-workflow/login.mdx @@ -58,4 +58,71 @@ * login + + ```csharp + + // Method to handle user login + public async void Login(string userName, string rtmToken) + { + try + { + var result= await signalingEngine.LoginAsync(rtmToken); + if(userName != "") + { + configData.uid = userName; + } + if (result.Status.Error) + { + LogError($"Error during login: {result.Status.Reason}"); + } + else + { + LogInfo($"Login successful. Response: {result.Response}"); + isLogin = true; + } + } + catch (Exception ex) + { + LogError($"Exception during login: {ex.Message}"); + } + } + ``` + * LoginAsync + + + ```cpp + void SignalingManagerAuthentication::loginWithToken(std::string userId) + { + std::cout << "Fetching token from the server..." << std::endl; + token = fetchRTMToken(userId); + uid = userId; + + RtmConfig rtmConfig; + rtmConfig.appId = appId.c_str(); + rtmConfig.userId = uid.c_str(); + rtmConfig.presenceTimeout = config["presenceTimeout"]; + rtmConfig.eventHandler = eventHandler_.get(); + + // Initialize the signalingEngine + int ret = signalingEngine->initialize(rtmConfig); + std::cout << "Initialize returned: " << ret << std::endl; + if (ret) + { + std::cout << "Error initializing Signaling service: " << ret << std::endl; + exit(0); + } + + // Log in using the token + ret = signalingEngine->login(token.c_str()); + std::cout << "Login returned: " << ret << std::endl; + + if (ret) + { + std::cout << "Login failed: " << ret << std::endl; + exit(0); + } + } + ``` + * login + diff --git a/assets/code/signaling/authentication-workflow/renew-token.mdx b/assets/code/signaling/authentication-workflow/renew-token.mdx index a1398368f..c720dcc32 100644 --- a/assets/code/signaling/authentication-workflow/renew-token.mdx +++ b/assets/code/signaling/authentication-workflow/renew-token.mdx @@ -64,4 +64,25 @@ ``` + + ```csharp + // Renew the RTM token + public void RenewToken(string token) + { + // Update engine with new token + signalingEngine.RenewTokenAsync(token); + } + ``` + + + + ```cpp + int SignalingManagerAuthentication::renewToken() + { + std::cout << "Fetching token to renew expiring token..." << std::endl; + token = fetchRTMToken(uid); + return signalingEngine->renewToken(token.c_str()); + } + ``` + diff --git a/assets/code/signaling/authentication-workflow/retrieve-rtc-token.mdx b/assets/code/signaling/authentication-workflow/retrieve-rtc-token.mdx index 11af44252..76dc2f276 100644 --- a/assets/code/signaling/authentication-workflow/retrieve-rtc-token.mdx +++ b/assets/code/signaling/authentication-workflow/retrieve-rtc-token.mdx @@ -77,4 +77,45 @@ ``` + + + ```csharp + // Fetch a rtc token + public async Task FetchRtcToken(string channelName, string uid) + { + + string url = string.Format("{0}/rtc/{1}/{2}/uid/{3}/?expiry={4}", configData.serverUrl, channelName , 1 , uid , configData.tokenExpiryTime); + UnityWebRequest request = UnityWebRequest.Get(url); + + var operation = request.SendWebRequest(); + + while (!operation.isDone) + { + await Task.Yield(); + } + + if (request.isNetworkError || request.isHttpError) + { + LogInfo(request.error); + return; + } + + RtcTokenStruct tokenInfo = JsonUtility.FromJson(request.downloadHandler.text); + Debug.Log("Retrieved rtc token : " + tokenInfo.rtcToken); + configData.rtcToken = tokenInfo.rtcToken; + } + ``` + + + + ```cpp + std::string SignalingManagerAuthentication::fetchRTCToken(std::string channelName) + { + // Build the URL with the channelName + int role = 1; + std::string url = serverUrl + "/rtc/" + channelName + "/" + std::to_string(role) + "/uid/" + uid + "/?expiry=" + std::to_string(tokenExpiryTime); + return fetchToken(url); + } + ``` + diff --git a/assets/code/signaling/authentication-workflow/retrieve-rtm-token.mdx b/assets/code/signaling/authentication-workflow/retrieve-rtm-token.mdx index 661975836..98b0e4cfb 100644 --- a/assets/code/signaling/authentication-workflow/retrieve-rtm-token.mdx +++ b/assets/code/signaling/authentication-workflow/retrieve-rtm-token.mdx @@ -136,3 +136,129 @@ } ``` + + ```csharp + // Asynchronously fetches an RTM token from the server + public async Task FetchRtmToken(string userName) + { + // Check if required parameters are provided in the configuration + if (string.IsNullOrEmpty(configData.uid) || string.IsNullOrEmpty(configData.serverUrl) || configData.tokenExpiryTime == null) + { + LogInfo("Please specify all required parameters in the config.json file to fetch a token from the server"); + return; + } + + // Use user name as UID + if (userName != "") + { + configData.uid = userName; + } + + // Construct the URL to request the RTM token + string url = $"{configData.serverUrl}/rtm/{configData.uid}/?expiry={configData.tokenExpiryTime}"; + + // Use UnityWebRequest to send a GET request to the server + UnityWebRequest request = UnityWebRequest.Get(url); + + // Asynchronously send the request + var operation = request.SendWebRequest(); + + // Wait until the operation is done + while (!operation.isDone) + { + await Task.Yield(); + } + + // Check for network or HTTP errors + if (request.isNetworkError || request.isHttpError) + { + LogError($"Failed to fetch token. Error: {request.error}"); + return; + } + + // Deserialize the response JSON into TokenStruct + RtmTokenStruct tokenInfo = JsonUtility.FromJson(request.downloadHandler.text); + + // Log the retrieved token + LogInfo($"Retrieved rtm token: {tokenInfo.rtmToken}"); + + // Update the configuration with the fetched token`1 + configData.token = tokenInfo.rtmToken; + } + ``` + + + + ```cpp + std::string SignalingManagerAuthentication::fetchRTMToken(std::string userId) + { + // Build the URL with the userId + std::string url = serverUrl + "/rtm/" + userId + "/?expiry=" + std::to_string(tokenExpiryTime); + return fetchToken(url); + } + + std::string SignalingManagerAuthentication::fetchToken(std::string url) + { + // Initialize cURL + CURL *curl = curl_easy_init(); + + if (curl) + { + // Set the URL for the GET request + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + + // Response data will be stored in this string + std::string responseData; + + // Set the callback function to handle the received data + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseData); + + // Perform the GET request + CURLcode res = curl_easy_perform(curl); + + // Check for errors + if (res != CURLE_OK) + { + std::cerr << "Failed to fetch token: " << curl_easy_strerror(res) << std::endl; + } + else + { + // Parse the JSON response + try + { + json responseJson = json::parse(responseData); + + // Extract the token value + std::string token; + if (url.find("rtm") != std::string::npos) + { + token = responseJson["rtmToken"]; + } + else + { + token = responseJson["rtcToken"]; + } + + // Cleanup and return the token + curl_easy_cleanup(curl); + return token; + } + catch (const std::exception &e) + { + std::cerr << "JSON parsing error: " << e.what() << std::endl; + } + } + + // Cleanup in case of error + curl_easy_cleanup(curl); + } + else + { + std::cerr << "cURL initialization failed." << std::endl; + } + + return ""; // Return an empty string in case of error + } + ``` + diff --git a/assets/code/signaling/engine-config/apply-encryption-config.mdx b/assets/code/signaling/engine-config/apply-encryption-config.mdx index 3a42c4027..5b52dc4ae 100644 --- a/assets/code/signaling/engine-config/apply-encryption-config.mdx +++ b/assets/code/signaling/engine-config/apply-encryption-config.mdx @@ -58,4 +58,18 @@ ``` * Initialization + + + + ```csharp + rtmConfig.encryptionConfig = encryptionConfig; + ``` + * RtmConfig + + + + ```cpp + int ret = signalingEngine->initialize(rtmConfig); + ``` + * initialize \ No newline at end of file diff --git a/assets/code/signaling/engine-config/apply-geofencing-config.mdx b/assets/code/signaling/engine-config/apply-geofencing-config.mdx index 575626d59..f3a0c1f25 100644 --- a/assets/code/signaling/engine-config/apply-geofencing-config.mdx +++ b/assets/code/signaling/engine-config/apply-geofencing-config.mdx @@ -54,6 +54,11 @@ ``` javascript signalingEngine = new AgoraRTM.RTM(config.appId, config.uid, rtmConfig); ``` - * Initialization - \ No newline at end of file + + + ```cpp + int ret = signalingEngine->initialize(rtmConfig); + ``` + * initialize + diff --git a/assets/code/signaling/engine-config/apply-proxy-config.mdx b/assets/code/signaling/engine-config/apply-proxy-config.mdx index 299bce8e4..3c0a4d954 100644 --- a/assets/code/signaling/engine-config/apply-proxy-config.mdx +++ b/assets/code/signaling/engine-config/apply-proxy-config.mdx @@ -57,4 +57,17 @@ * Initialization + + + ```csharp + rtmConfig.proxyConfig = config; + ``` + * RtmConfig + + + + ```cpp + int ret = signalingEngine->initialize(rtmConfig); + ``` + * initialize \ No newline at end of file diff --git a/assets/code/signaling/engine-config/cloud-proxy.mdx b/assets/code/signaling/engine-config/cloud-proxy.mdx index 5b48cc536..00d157ae4 100644 --- a/assets/code/signaling/engine-config/cloud-proxy.mdx +++ b/assets/code/signaling/engine-config/cloud-proxy.mdx @@ -43,4 +43,30 @@ ``` * rtmConfig + + + ```csharp + // Define proxy configuration + RtmProxyConfig config = new RtmProxyConfig(); + config.server = ""; // server URL + config.port = 8080; // server port + config.account = ""; // Account name + config.password = ""; // Account password + ``` + * RtmProxyConfig + + + + ```cpp + RtmProxyConfig proxyConfig; + proxyConfig.account = ""; // Proxy login account + proxyConfig.password = ""; // Proxy login password + proxyConfig.port = 1234; // Proxy listening port + proxyConfig.proxyType = RTM_PROXY_TYPE_HTTP; // Proxy type + proxyConfig.server = ""; // Proxy server domain name or IP address + rtmConfig.proxyConfig = proxyConfig; + ``` + * RtmConfig + * RtmProxyConfig + \ No newline at end of file diff --git a/assets/code/signaling/engine-config/encryption-config.mdx b/assets/code/signaling/engine-config/encryption-config.mdx index 2a59c3fe9..3a015113a 100644 --- a/assets/code/signaling/engine-config/encryption-config.mdx +++ b/assets/code/signaling/engine-config/encryption-config.mdx @@ -119,4 +119,24 @@ * rtmConfig + + + ```csharp + RtmEncryptionConfig encryptionConfig = new RtmEncryptionConfig(); + encryptionConfig.encryptionKey = configData.cipherKey; + encryptionConfig.encryptionSalt = System.Text.Encoding.UTF8.GetBytes(configData.salt); + encryptionConfig.encryptionMode = RTM_ENCRYPTION_MODE.AES_256_GCM; + ``` + * RtmEncryptionConfig + + + + ```cpp + uint8_t salt[32] = {1,2,3,4,5,6,7,8}; + rtmConfig.encryptionConfig.encryptionKey = "your_encryption_key"; + rtmConfig.encryptionConfig.encryptionMode = RTM_ENCRYPTION_MODE_AES_256_GCM; + memcpy(rtmConfig.encryptionConfig.encryptionSalt, salt, 32); + ``` + * RtmConfig + \ No newline at end of file diff --git a/assets/code/signaling/engine-config/set-area.mdx b/assets/code/signaling/engine-config/set-area.mdx index d8df32d27..a6bb26059 100644 --- a/assets/code/signaling/engine-config/set-area.mdx +++ b/assets/code/signaling/engine-config/set-area.mdx @@ -41,4 +41,18 @@ - `JAPAN`: Japan. - `NORTH_AMERICA`: North America. + + + + ```csharp + rtmConfig.areaCode = Agora.Rtm.RTM_AREA_CODE.GLOB; + ``` + * RtmConfig + + + + ```cpp + rtmConfig.areaCode = RTM_AREA_CODE_NA; + ``` + * RtmConfig \ No newline at end of file diff --git a/assets/code/signaling/get-started-sdk/configure-engine-instance.mdx b/assets/code/signaling/get-started-sdk/configure-engine-instance.mdx index 6c83832c6..1becbb4b8 100644 --- a/assets/code/signaling/get-started-sdk/configure-engine-instance.mdx +++ b/assets/code/signaling/get-started-sdk/configure-engine-instance.mdx @@ -70,3 +70,81 @@ * Initialization + + + ```csharp + // Method to set up the signaling engine + public virtual void SetupSignalingEngine() + { + try + { + rtmConfig.appId = configData.appId; + rtmConfig.userId = configData.uid; + rtmConfig.useStringUserId = true; + rtmConfig.logConfig = null; + signalingEngine = RtmClient.CreateAgoraRtmClient(rtmConfig); + + } + catch (RTMException e) + { + LogError($"Error initializing RtmClient: {e.Status.ErrorCode}"); + } + // Register event handlers + if (signalingEngine != null) + { + RegisterEventHandlers(); + } + } + + // Method to register event handlers for the signaling engine + public void RegisterEventHandlers() + { + signalingEngine.OnMessageEvent += OnMessageEvent; + signalingEngine.OnPresenceEvent += OnPresenceEvent; + signalingEngine.OnTopicEvent += OnTopicEvent; + signalingEngine.OnStorageEvent += OnStorageEvent; + signalingEngine.OnLockEvent += OnLockEvent; + signalingEngine.OnConnectionStateChanged += OnConnectStateChanged; + signalingEngine.OnTokenPrivilegeWillExpire += OnTokenPrivilegeWillExpire; + } + ``` + - RtmConfig + - CreateAgoraRtmClient + + + + + ```cpp + // Create an IRtmClient instance + signalingEngine = createAgoraRtmClient(); + if (!signalingEngine) + { + std::cout << "Error creating Signaling service!" << std::endl; + exit(0); + } + else + { + // Success creating an IRtmClient instance + } + + RtmConfig rtmConfig; + rtmConfig.appId = appId.c_str(); + rtmConfig.userId = uid.c_str(); + rtmConfig.presenceTimeout = config["presenceTimeout"]; + rtmConfig.eventHandler = eventHandler_.get(); + + // Initialize the signalingEngine + int ret = signalingEngine->initialize(rtmConfig); + std::cout << "initialize returned: " << ret << std::endl; + + if (ret) + { + std::cout << "Error initializing Signaling service: " << ret << std::endl; + exit(0); + } + ``` + * createAgoraRtmClient + * RtmConfig + * initialize + + diff --git a/assets/code/signaling/get-started-sdk/declare-variables.mdx b/assets/code/signaling/get-started-sdk/declare-variables.mdx index e19fa01a5..ba5dbab18 100644 --- a/assets/code/signaling/get-started-sdk/declare-variables.mdx +++ b/assets/code/signaling/get-started-sdk/declare-variables.mdx @@ -1,4 +1,4 @@ - + ```json { @@ -21,4 +21,23 @@ } ``` + + + + +```json + { + "uid": "101", + "appId": "", + "channelName": "demo", + "token": "", + "serverUrl": "", + "tokenExpiryTime": 150, + "presenceTimeout": 300, + "logUpload": false, + "logLevel": "debug", + "useStringUserId": false, + "rtcToken": "" + } +``` \ No newline at end of file diff --git a/assets/code/signaling/get-started-sdk/engine-instance-variable.mdx b/assets/code/signaling/get-started-sdk/engine-instance-variable.mdx index f0df12eb1..e52439c01 100644 --- a/assets/code/signaling/get-started-sdk/engine-instance-variable.mdx +++ b/assets/code/signaling/get-started-sdk/engine-instance-variable.mdx @@ -21,7 +21,6 @@ - RtmClientKit - ```typescript @@ -29,5 +28,14 @@ let signalingEngine = null; let signalingChannel = null; ``` - + + + ```csharp + internal IRtmClient signalingEngine; + ``` + + + ```cpp + IRtmClient *signalingEngine; + ``` \ No newline at end of file diff --git a/assets/code/signaling/get-started-sdk/login.mdx b/assets/code/signaling/get-started-sdk/login.mdx index 0fff15c05..66cf4f02b 100644 --- a/assets/code/signaling/get-started-sdk/login.mdx +++ b/assets/code/signaling/get-started-sdk/login.mdx @@ -76,4 +76,55 @@ * login + + ```csharp + // Method to handle user login + public async void Login(string userName, string rtmToken) + { + if (signalingEngine == null) + { + SetupSignalingEngine(); + } + try + { + var result = await signalingEngine.LoginAsync(rtmToken); + if (result.Status.Error) + { + LogError($"Error during login: {result.Status.Reason}"); + } + else + { + LogInfo($"Login successful. Response: {result.Response}"); + isLogin = true; + } + } + catch (Exception ex) + { + LogError($"Exception during login: {ex.Message}"); + } + } + ``` + * LoginAsync + + + + ```cpp + void SignalingManager::login() + { + // Log in using the token + ret = signalingEngine->login(token.c_str()); + if (ret) + { + std::cout << "login failed: " << ret << std::endl; + exit(0); + } + else + { + std::cout << "login returned: " << ret << std::endl; + } + } + ``` + * login + + diff --git a/assets/code/signaling/get-started-sdk/logout.mdx b/assets/code/signaling/get-started-sdk/logout.mdx index 1e9f5971f..7eead76fd 100644 --- a/assets/code/signaling/get-started-sdk/logout.mdx +++ b/assets/code/signaling/get-started-sdk/logout.mdx @@ -58,4 +58,30 @@ * logout + + ```csharp + // Method to handle user logout + public void Logout() + { + signalingEngine?.LogoutAsync(); + isLogin = false; + isSubscribed = false; + DestroyEngine(); + } + ``` + * LogoutAsync + + + + ```cpp + // Log out from the RTM server + void SignalingManager::logout() + { + int ret = signalingEngine->logout(); + std::cout << "logout returned: " << ret << std::endl; + } + ``` + * logout + + diff --git a/assets/code/signaling/get-started-sdk/publish.mdx b/assets/code/signaling/get-started-sdk/publish.mdx index 5765e121b..6c8ad7aa3 100644 --- a/assets/code/signaling/get-started-sdk/publish.mdx +++ b/assets/code/signaling/get-started-sdk/publish.mdx @@ -74,4 +74,42 @@ * publish + + ```csharp + // Method to send a message to a channel + public async void SendChannelMessage(string msg) + { + PublishOptions options = new PublishOptions(); + options.channelType = RTM_CHANNEL_TYPE.MESSAGE; + options.customType = "string"; + var result = await signalingEngine.PublishAsync(configData.channelName, msg, options); + if (result.Status.Error) + { + LogError(string.Format("rtmClient.Publish Status.ErrorCode:{0} ", result.Status.ErrorCode)); + } + else + { + string info = string.Format("Message published successfully"); + LogInfo(info); + } + } + ``` + * PublishAsync + + + + ```cpp + // Publish a message + void SignalingManager::publishMessage(std::string chn, std::string msg) + { + PublishOptions opt; + opt.messageType = RTM_MESSAGE_TYPE_STRING; + uint64_t requestId; + int ret = signalingEngine->publish(chn.c_str(), msg.c_str(), msg.size(), opt, requestId); + std::cout << "publishMessage returned: " << ret << std::endl; + } + ``` + * publish + + diff --git a/assets/code/signaling/get-started-sdk/respond-to-events.mdx b/assets/code/signaling/get-started-sdk/respond-to-events.mdx index 58267b78c..2d4305439 100644 --- a/assets/code/signaling/get-started-sdk/respond-to-events.mdx +++ b/assets/code/signaling/get-started-sdk/respond-to-events.mdx @@ -151,4 +151,180 @@ * Event Listeners + + ```csharp + // Method to handle message events + public async void OnMessageEvent(MessageEvent @event) + { + string str = $"OnMessageEvent channelName:{@event.channelName} channelTopic:{@event.channelTopic} " + + $"channelType:{@event.channelType} publisher:{@event.publisher} " + + $"message:{@event.message.GetData()} customType:{@event.customType}"; + Debug.Log(str); + string msg = @event.publisher.ToString() + ": " + @event.message.GetData(); + await GetOnlineMembersInChannel(configData.channelName); + } + + // Method to handle presence events + public virtual void OnPresenceEvent(PresenceEvent @event) + { + string str = $"OnPresenceEvent: type:{@event.type} channelType:{@event.channelType} " + + $"channelName:{@event.channelName} publisher:{@event.publisher}"; + Debug.Log(str); + } + + // Method to handle storage events + public void OnStorageEvent(StorageEvent @event) + { + string str = $"OnStorageEvent: channelType:{@event.channelType} storageType:{@event.storageType} " + + $"eventType:{@event.eventType} target:{@event.target}"; + Debug.Log(str); + } + + // Method to handle topic events + public void OnTopicEvent(TopicEvent @event) + { + string str = $"OnTopicEvent: channelName:{@event.channelName} publisher:{@event.publisher}"; + var topicInfoCount = @event.topicInfos?.Length ?? 0; + Debug.Log(str); + if (topicInfoCount > 0) + { + for (var i = 0; i < topicInfoCount; i++) + { + var topicInfo = @event.topicInfos[i]; + var publisherCount = topicInfo.publishers?.Length ?? 0; + string str1 = $"|--topicInfo {i}: topic:{topicInfo.topic} publisherCount:{publisherCount}"; + + if (publisherCount > 0) + { + for (var j = 0; j < publisherCount; j++) + { + var publisher = topicInfo.publishers[j]; + string str2 = $" |--publisher {j}: userId:{publisher.publisherUserId} meta:{publisher.publisherMeta}"; + Debug.Log(str2); + } + } + Debug.Log(str1); + } + } + } + + // Method to handle lock events + public void OnLockEvent(LockEvent @event) + { + var count = @event.lockDetailList?.Length ?? 0; + string info = $"OnLockEvent channelType:{@event.channelType}, eventType:{@event.eventType}, " + + $"channelName:{@event.channelName}, count:{count}"; + + if (count > 0) + { + for (var i = 0; i < count; i++) + { + var detail = @event.lockDetailList[i]; + string info2 = $"lockDetailList lockName:{detail.lockName}, owner:{detail.owner}, ttl:{detail.ttl}"; + Debug.Log(info2); + } + } + Debug.Log(info); + } + + // Method to handle connection state change events + public void OnConnectStateChanged(string channelName, RTM_CONNECTION_STATE state, RTM_CONNECTION_CHANGE_REASON reason) + { + string str = $"OnConnectStateChanged channelName:{channelName} current state: {state} reason: {reason}"; + LogInfo(str); + } + + // Method to handle token privilege will expire event + public virtual void OnTokenPrivilegeWillExpire(string channelName) + { + string str1 = $"OnTokenPrivilegeWillExpire channelName: {channelName}"; + Debug.Log(str1); + } + ``` + * Event Listeners + + + + + ```cpp + class BaseSignalingEventHandler : public IRtmEventHandler + { + public: + BaseSignalingEventHandler(SignalingManager *manager); + // Add the event listener + void onMessageEvent(const MessageEvent& event) override; + void onPresenceEvent(const PresenceEvent& event) override; + void onLoginResult(RTM_ERROR_CODE errorCode) override; + void onConnectionStateChanged(const char *channelName, RTM_CONNECTION_STATE state, RTM_CONNECTION_CHANGE_REASON reason) override; + void onPublishResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) override; + void onSubscribeResult(const uint64_t requestId, const char *channelName, RTM_ERROR_CODE errorCode) override; + + protected: + void cbPrint(const char *fmt, ...); + SignalingManager *signalingManager; + }; + ``` + + ```cpp + BaseSignalingEventHandler::BaseSignalingEventHandler(SignalingManager *manager) + : signalingManager(manager) + { } + + void BaseSignalingEventHandler::onLoginResult(RTM_ERROR_CODE errorCode) + { + cbPrint("onLogin: errorCode: %d", errorCode); + } + + void BaseSignalingEventHandler::onConnectionStateChanged(const char *channelName, RTM_CONNECTION_STATE state, RTM_CONNECTION_CHANGE_REASON reason) + { + std::string description = SignalingManager::getConnectionStateDescription(state) + "\n"; + cbPrint(description.c_str()); + bool isLoggedIn = (state == RTM_CONNECTION_STATE_CONNECTED); + signalingManager->updateLoginStatus(isLoggedIn); + } + + void BaseSignalingEventHandler::onPublishResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) + { + cbPrint("onPublishResult request id: %llu result: %d", requestId, errorCode); + } + + void BaseSignalingEventHandler::onMessageEvent(const MessageEvent &event) + { + cbPrint("received message from: %s, message: %s", event.publisher, event.message); + } + + void BaseSignalingEventHandler::onPresenceEvent(const PresenceEvent &event) + { + if (event.type == RTM_PRESENCE_EVENT_TYPE_REMOTE_JOIN_CHANNEL) + { + cbPrint("presence: remote user joined, publisher: %d", event.type, event.publisher); + } + else if (event.type == RTM_PRESENCE_EVENT_TYPE_REMOTE_LEAVE_CHANNEL) + { + cbPrint("presence: remote user left, publisher: %d", event.type, event.publisher); + } + else + { + cbPrint("presence event: %d, publisher: %s", event.type, event.publisher); + } + } + + void BaseSignalingEventHandler::onSubscribeResult(const uint64_t requestId, const char *channelName, RTM_ERROR_CODE errorCode) + { + cbPrint("onSubscribeResult: channel:%s, request id: %llu result: %d", channelName, requestId, errorCode); + } + + void BaseSignalingEventHandler::cbPrint(const char *fmt, ...) + { + printf("\x1b[32m<< RTM async callback: "); + va_list args; + va_start(args, fmt); + vprintf(fmt, args); + va_end(args); + printf(" >>\x1b[0m\n"); + } + ``` + * Event Listeners + + diff --git a/assets/code/signaling/get-started-sdk/subscribe.mdx b/assets/code/signaling/get-started-sdk/subscribe.mdx index d9434cb96..1a63c312d 100644 --- a/assets/code/signaling/get-started-sdk/subscribe.mdx +++ b/assets/code/signaling/get-started-sdk/subscribe.mdx @@ -67,4 +67,39 @@ * subscribe + + ```csharp + // Method to subscribe to a channel + public async void Subscribe() + { + SubscribeOptions subscribeOptions = new SubscribeOptions() + { + withMessage = true, + withMetadata = true, + withPresence = true, + withLock = true + }; + await signalingEngine.SubscribeAsync(configData.channelName, subscribeOptions); + await GetOnlineMembersInChannel(configData.channelName); + isSubscribed = true; + } + ``` + * SubscribeAsync + + + + ```cpp + // Subscribe to a channel + void SignalingManager::subscribeChannel(std::string chnId) + { + channelType = RTM_CHANNEL_TYPE_MESSAGE; + SubscribeOptions opt = SubscribeOptions(); + uint64_t requestId; + int ret = signalingEngine->subscribe(chnId.c_str(), opt, requestId); + std::cout << "subscribe channel returned: " << ret << std::endl; + if (ret == 0) channelName = chnId; + } + ``` + * subscribe + diff --git a/assets/code/signaling/get-started-sdk/unsubscribe.mdx b/assets/code/signaling/get-started-sdk/unsubscribe.mdx index d26968796..a071e68b8 100644 --- a/assets/code/signaling/get-started-sdk/unsubscribe.mdx +++ b/assets/code/signaling/get-started-sdk/unsubscribe.mdx @@ -54,4 +54,30 @@ * unsubscribe + + ```csharp + // Method to unsubscribe from a channel + public async void Unsubscribe() + { + await signalingEngine.UnsubscribeAsync(configData.channelName); + signalingChannel = null; + isSubscribed = false; + } + ``` + * UnsubscribeAsync + + + + ```cpp + // Unsubscribe from a channel + void SignalingManager::unsubscribeChannel(std::string chnId) + { + uint64_t requestId; + int ret = signalingEngine->unsubscribe(chnId.c_str()); + std::cout << "unsubscribe channel returned: " << ret << std::endl; + } + ``` + * unsubscribe + + diff --git a/assets/code/signaling/presence/enable-notifications.mdx b/assets/code/signaling/presence/enable-notifications.mdx index e3f52dc36..dbc9df633 100644 --- a/assets/code/signaling/presence/enable-notifications.mdx +++ b/assets/code/signaling/presence/enable-notifications.mdx @@ -71,4 +71,24 @@ * subscribe + + ```csharp + // Method to subscribe to a channel + public async void Subscribe() + { + SubscribeOptions subscribeOptions = new SubscribeOptions() + { + withMessage = true, + withMetadata = true, + withPresence = true, + withLock = true + }; + await signalingEngine.SubscribeAsync(configData.channelName, subscribeOptions); + await GetOnlineMembersInChannel(configData.channelName); + isSubscribed = true; + } + ``` + * SubscribeAsync + + diff --git a/assets/code/signaling/presence/event-listener.mdx b/assets/code/signaling/presence/event-listener.mdx index 1d02c7b6a..4edd4fc6f 100644 --- a/assets/code/signaling/presence/event-listener.mdx +++ b/assets/code/signaling/presence/event-listener.mdx @@ -67,4 +67,104 @@ ``` * Event Listeners + + ```csharp + // Subscribe to the PresenceEvent using a lambda expression + signalingEngine.OnPresenceEvent += (presenceEvent) => + { + // Build a formatted log message with presence event details + string logMessage = $"OnPresenceEvent: type:{presenceEvent.type} " + + $"channelType:{presenceEvent.channelType} " + + $"channelName:{presenceEvent.channelName} " + + $"publisher:{presenceEvent.publisher}"; + + // Log the presence event details to the console + Debug.Log(logMessage); + }; + ``` + * Event Listeners + + + + ```cpp + void PresenceEventHandler::onGetOnlineUsersResult(const uint64_t requestId, + const UserState *userStateList, const size_t count, + const char *nextPage, RTM_ERROR_CODE errorCode) + { + if (errorCode != RTM_ERROR_OK) + { + printf("getOnlineUsers failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode)); + } + else + { + printf("List of users in the channel:\n"); + for (int i = 0; i < count; i++) + { + printf("user: %s\n", userStateList[i].userId); + for (int j = 0; j < userStateList[i].statesCount; j++) + { + printf("key: %s value: %s\n", userStateList[i].states[j].key, userStateList[i].states[j].value); + } + } + } + } + + void PresenceEventHandler::onGetUserChannelsResult(const uint64_t requestId, const ChannelInfo *channels, const size_t count, RTM_ERROR_CODE errorCode) + { + if (errorCode != RTM_ERROR_OK) + { + printf("getUserChannels failed error: %d, reason: %s\n", errorCode, getErrorReason(errorCode)); + } + else + { + printf("User is in the following channels:\n"); + for (int i = 0; i < count; i++) + { + printf("channel: %s channel type: %d\n", channels[i].channelName, channels[i].channelType); + } + } + } + + void PresenceEventHandler::onPresenceSetStateResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) + { + if (errorCode != RTM_ERROR_OK) + { + printf("SetState failed error: %d reason: %s\n", errorCode, getErrorReason(errorCode)); + } + else + { + printf("SetState success\n"); + } + } + + void PresenceEventHandler::onPresenceGetStateResult(const uint64_t requestId, const UserState &state, RTM_ERROR_CODE errorCode) + { + if (errorCode != RTM_ERROR_OK) + { + printf("GetState failed error: %d reason: %s\n", errorCode, getErrorReason(errorCode)); + } + else + { + printf("GetState user id: %s success\n", state.userId); + for (int i = 0; i < state.statesCount; i++) + { + printf("key: %s, value: %s\n", state.states[i].key, state.states[i].value); + } + } + } + + void PresenceEventHandler::onPresenceRemoveStateResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) + { + if (errorCode != RTM_ERROR_OK) + { + printf("RemoveState failed error: %d reason: %s\n", errorCode, getErrorReason(errorCode)); + } + else + { + printf("RemoveState success\n"); + } + } + ``` + * Event Listeners + diff --git a/assets/code/signaling/presence/get-user-status.mdx b/assets/code/signaling/presence/get-user-status.mdx index aee7d8e3a..c3b6b24f9 100644 --- a/assets/code/signaling/presence/get-user-status.mdx +++ b/assets/code/signaling/presence/get-user-status.mdx @@ -61,4 +61,55 @@ ``` * getState + + + ```csharp + var result = await signalingEngine.GetPresence().GetStateAsync(channelName, RTM_CHANNEL_TYPE.MESSAGE, userId); + if (result.Status.Error) + { + Debug.Log(string.Format("{0} is failed, ErrorCode: {1}, due to: {2}", result.Status.Operation, result.Status.ErrorCode, result.Status.Reason)); + } + else + { + Debug.Log(string.Format("User:{0}, have stateCount:{1} states", result.Response.State.userId, result.Responses.State.states.Length)); + } + ``` + * GetStateAsync + + + + + ```cpp + void SignalingManagerPresence::getState(std::string channelName, std::string userId) + { + uint64_t requestId; // Output parameter used to identify and process the result + + int ret = signalingEngine->getPresence()->getState(channelName.c_str(), RTM_CHANNEL_TYPE_MESSAGE, userId.c_str(), requestId); + + if (ret != RTM_ERROR_OK) + { + printf("getState failed error is %d reason is %s\n", ret, getErrorReason(ret)); + } + } + ``` + + ```cpp + void PresenceEventHandler::onPresenceGetStateResult(const uint64_t requestId, const UserState &state, RTM_ERROR_CODE errorCode) + { + if (errorCode != RTM_ERROR_OK) + { + printf("GetState failed error: %d reason: %s\n", errorCode, getErrorReason(errorCode)); + } + else + { + printf("GetState user id: %s success\n", state.userId); + for (int i = 0; i < state.statesCount; i++) + { + printf("key: %s, value: %s\n", state.states[i].key, state.states[i].value); + } + } + } + ``` + * getState + \ No newline at end of file diff --git a/assets/code/signaling/presence/list-users.mdx b/assets/code/signaling/presence/list-users.mdx index 44d392f92..35011c1a4 100644 --- a/assets/code/signaling/presence/list-users.mdx +++ b/assets/code/signaling/presence/list-users.mdx @@ -62,3 +62,69 @@ * getOnlineUsers * getUserChannels + + ```csharp + // Method to get online members in a channel + public async Task GetOnlineMembersInChannel(string channel) + { + GetOnlineUsersOptions options = new GetOnlineUsersOptions(); + options.includeState = true; + options.includeUserId = true; + + IRtmPresence rtmPresence = signalingEngine.GetPresence(); + var result = await rtmPresence.GetOnlineUsersAsync(configData.channelName, RTM_CHANNEL_TYPE.MESSAGE, options); + userStateList = result.Response.UserStateList; + userCount = result.Response.UserStateList.Length; + string info = $"GetOnlineUsers Response: count:{userCount}, nextPage:{result.Response.NextPage}"; + LogInfo(info); + } + ``` + * GetOnlineUsersAsync + * GetUserChannelsAsync + + + + ```cpp + void SignalingManagerPresence::getOnlineUsers(std::string channelName) + { + GetOnlineUsersOptions options; + options.includeState = true; // Include user IDs of the online users + options.includeUserId = true; // Include temporary status data of online users + uint64_t requestId; // Output parameter used to identify and process the result + + int ret = signalingEngine->getPresence()->getOnlineUsers(channelName.c_str(), RTM_CHANNEL_TYPE_MESSAGE, options, requestId); + if (ret != RTM_ERROR_OK) + { + printf("getOnlineUsers failed error is %d reason is %s\n", ret, getErrorReason(ret)); + } + } + ``` + + ```cpp + oid PresenceEventHandler::onGetOnlineUsersResult(const uint64_t requestId, + const UserState *userStateList, const size_t count, + const char *nextPage, RTM_ERROR_CODE errorCode) + { + + if (errorCode != RTM_ERROR_OK) + { + printf("getOnlineUsers failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode)); + } + else + { + printf("List of users in the channel:\n"); + for (int i = 0; i < count; i++) + { + printf("user: %s\n", userStateList[i].userId); + for (int j = 0; j < userStateList[i].statesCount; j++) + { + printf("key: %s value: %s\n", userStateList[i].states[j].key, userStateList[i].states[j].value); + } + } + } + } + ``` + * getOnlineUsers + * getUserChannels + + \ No newline at end of file diff --git a/assets/code/signaling/presence/set-user-status.mdx b/assets/code/signaling/presence/set-user-status.mdx index 2497305c3..ac9ec6766 100644 --- a/assets/code/signaling/presence/set-user-status.mdx +++ b/assets/code/signaling/presence/set-user-status.mdx @@ -62,6 +62,43 @@ }; ``` + + ```csharp + var stateItems = new StateItem[1]; + stateItems[0] = new StateItem("state", "Online"); + var result = await signalingEngine.GetPresence().SetStateAsync(channelName, RTM_CHANNEL_TYPE.MESSAGE, stateItems); + if (result.Status.Error) + { + Debug.Log(string.Format("{0} is failed, ErrorCode: {1}, due to: {2}", result.Status.Operation, result.Status.ErrorCode, result.Status.Reason)); + } + else + { + Debug.Log("Set State Success!"); + } + ``` + - SetStateAsync + + + + ```cpp + void SignalingManagerPresence::setState(std::string channelName, std::string key, std::string value) + { + StateItem states[1]; + states[0] = StateItem(); + states[0].key = key.c_str(); + states[0].value = value.c_str(); + uint64_t requestId; // Output parameter used to identify and process the result + + int ret = signalingEngine->getPresence()->setState(channelName.c_str(), RTM_CHANNEL_TYPE_MESSAGE, states, 1, requestId); + if (ret != RTM_ERROR_OK) + { + printf("setState failed error: %d reason: %s\n", ret, getErrorReason(ret)); + } + } + ``` + * setState + + diff --git a/assets/code/signaling/storage/get-channel-metadata.mdx b/assets/code/signaling/storage/get-channel-metadata.mdx index 306c11225..e9a2fbd75 100644 --- a/assets/code/signaling/storage/get-channel-metadata.mdx +++ b/assets/code/signaling/storage/get-channel-metadata.mdx @@ -64,3 +64,62 @@ * getChannelMetadata + + ```csharp + // Get channel metadata + public async void GetChannelMetadata(string channelName, RTM_CHANNEL_TYPE type) + { + IRtmStorage rtmStorage = signalingEngine.GetStorage(); + // Get channel metadata asynchronously + var result = await rtmStorage.GetChannelMetadataAsync(channelName, type); + if (result.Status.Error) + { + LogError($"IRtmStorage.UnsubscribeUserMetadata ret: {result.Status.Reason}"); + } + else + { + LogInfo($"getChannelMetadata channelName :{result.Response.ChannelName}"); + DisplayRtmMetadata(ref result.Response.Data); + } + } + ``` + * GetChannelMetadataAsync + + + + ```cpp + void SignalingManagerStorage::getChannelMetadata(std::string channelName) + { + uint64_t requestId; // Output parameter used to identify and process the result + int ret = signalingEngine->getStorage()->getChannelMetadata(channelName.c_str(), channelType, requestId); + + if (ret != RTM_ERROR_OK) + { + printf("getChannelMetadata failed error: %d reason: %s\n", ret, getErrorReason(ret)); + } + } + ``` + + ```cpp + void StorageEventHandler::onGetChannelMetadataResult(const uint64_t requestId, const char *channelName, RTM_CHANNEL_TYPE channelType, const IMetadata &data, RTM_ERROR_CODE errorCode) + { + if (errorCode != RTM_ERROR_OK) + { + printf("getChannelMetadata failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode)); + } + else + { + printf("getChannelMetadata success\n"); + const MetadataItem *items; + size_t size; + data.getMetadataItems(&items, &size); + for (int i = 0; i < size; i++) + { + printf("key: %s value: %s revison: %ld\n", items[i].key, items[i].value, items[i].revision); + } + } + } + ``` + * getChannelMetadata + + diff --git a/assets/code/signaling/storage/get-user-metadata.mdx b/assets/code/signaling/storage/get-user-metadata.mdx index 2b6e71d86..6df6d3051 100644 --- a/assets/code/signaling/storage/get-user-metadata.mdx +++ b/assets/code/signaling/storage/get-user-metadata.mdx @@ -61,4 +61,63 @@ * getUserMetadata + + ```csharp + // Get user metadata + public async void GetUserMetadata(string uid) + { + IRtmStorage rtmStorage = signalingEngine.GetStorage(); + // Get user metadata asynchronously + var result = await rtmStorage.GetUserMetadataAsync(uid); + if (result.Status.Error) + { + LogError($"GetUserMetadata Status.Reason:{result.Status.Reason}"); + } + else + { + LogInfo($"GetUserMetadata Response ,userId:{result.Response.UserId}"); + DisplayRtmMetadata(ref result.Response.Data); + } + } + ``` + * GetUserMetadataAsync + + + + + ```cpp + void SignalingManagerStorage::getUserMetadata(std::string userId) + { + uint64_t requestId; // Output parameter used to identify and process the result + int ret = signalingEngine->getStorage()->getUserMetadata(userId.c_str(), requestId); + + if (ret != RTM_ERROR_OK) + { + printf("getUserMetadata failed error: %d reason: %s\n", ret, getErrorReason(ret)); + } + } + ``` + + ```cpp + void StorageEventHandler::onGetUserMetadataResult(const uint64_t requestId, const char *userId, const IMetadata &data, RTM_ERROR_CODE errorCode) + { + if (errorCode != RTM_ERROR_OK) + { + printf("getUserMetadata failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode)); + } + else + { + printf("getUserMetadata success user id: %s\n", userId); + const MetadataItem *items; + size_t size; + data.getMetadataItems(&items, &size); + for (int i = 0; i < size; i++) + { + printf("key: %s value: %s revison: %ld\n", items[i].key, items[i].value, items[i].revision); + } + } + } + ``` + * getUserMetadata + \ No newline at end of file diff --git a/assets/code/signaling/storage/handle-metadata-events.mdx b/assets/code/signaling/storage/handle-metadata-events.mdx index 791203633..34dd13908 100644 --- a/assets/code/signaling/storage/handle-metadata-events.mdx +++ b/assets/code/signaling/storage/handle-metadata-events.mdx @@ -82,3 +82,19 @@ * Event Listeners + + + ```cpp + void StorageEventHandler::onStorageEvent(const StorageEvent &event) + { + cbPrint(("Storage event: " + getStorageEventDescription(event.eventType)).c_str()); + } + + void StorageEventHandler::onLockEvent(const LockEvent &event) + { + cbPrint(("Lock event: " + getLockEventDescription(event.eventType)).c_str()); + } + ``` + * Event Listeners + + diff --git a/assets/code/signaling/storage/manage-locks.mdx b/assets/code/signaling/storage/manage-locks.mdx index 225976680..5ff861ce5 100644 --- a/assets/code/signaling/storage/manage-locks.mdx +++ b/assets/code/signaling/storage/manage-locks.mdx @@ -189,4 +189,254 @@ * Lock + + ```csharp + // Set a lock + public async void SetLock(string lockName, int ttl) + { + IRtmLock rtmLock = signalingEngine.GetLock(); + // Set lock asynchronously + var result = await rtmLock.SetLockAsync(configData.channelName, RTM_CHANNEL_TYPE.MESSAGE, lockName, ttl); + + if (result.Status.Error) + { + Debug.LogError($"SetLock Status.Reason:{result.Status.Reason}"); + } + else + { + Debug.Log($"SetLock Response :channelName:{result.Response.ChannelName}, channelType:{result.Response.ChannelType}, lockName:{result.Response.LockName}"); + } + } + + // Get all locks + public async void GetLocks() + { + IRtmLock rtmLock = signalingEngine.GetLock(); + // Get locks asynchronously + var result = await rtmLock.GetLocksAsync(configData.channelName, RTM_CHANNEL_TYPE.MESSAGE); + + if (result.Status.Error) + { + Debug.LogError($"GetLocks Status.Reason:{result.Status.Reason}"); + } + else + { + var LockDetailListCount = result.Response.LockDetailList == null ? 0 : result.Response.LockDetailList.Length; + Debug.Log($"GetLocks Response: channelName:{result.Response.ChannelName},channelType:{result.Response.ChannelType},count:{LockDetailListCount}"); + if (LockDetailListCount > 0) + { + for (int i = 0; i < result.Response.LockDetailList.Length; i++) + { + var detail = result.Response.LockDetailList[i]; + Debug.Log($"{i} lockName:{detail.lockName}, owner:{detail.owner}, ttl:{detail.ttl}"); + } + } + } + } + + // Remove a lock + public async void RemoveLock(string lockName, string channelName, RTM_CHANNEL_TYPE channelType) + { + if (isLogin == false) + { + LogError("Login to remove the lock"); + return; + } + IRtmLock rtmLock = signalingEngine.GetLock(); + + // Remove lock asynchronously + var result = await rtmLock.RemoveLockAsync(channelName, channelType, lockName); + if (result.Status.Error) + { + Debug.LogError($"RemoveLock Status.Reason:{result.Status.Reason}"); + } + else + { + Debug.Log($"RemoveLock Response channelName:{result.Response.ChannelName},channelType:{result.Response.ChannelType},lockName:{result.Response.LockName}"); + } + } + + // Release a lock + public async void ReleaseLock(string lockName, string channelName) + { + IRtmLock rtmLock = signalingEngine.GetLock(); + + // Release lock asynchronously + var result = await rtmLock.ReleaseLockAsync(channelName, RTM_CHANNEL_TYPE.MESSAGE, lockName); + + if (result.Status.Error) + { + Debug.LogError($"ReleaseLock Status.Reason:{result.Status.Reason}"); + } + else + { + Debug.Log($"ReleaseLock Response:channelName:{result.Response.ChannelName},channelType:{result.Response.ChannelType},lockName:{result.Response.LockName}"); + } + } + + // Acquire a lock + public async void AcquireLock(string lockName) + { + IRtmLock rtmLock = signalingEngine.GetLock(); + // Acquire lock asynchronously + var result = await rtmLock.AcquireLockAsync(configData.channelName, RTM_CHANNEL_TYPE.MESSAGE, lockName, true); + + if (result.Status.Error) + { + Debug.LogError($"AcquireLock Status.Reason:{result.Status.Reason}"); + } + else + { + Debug.Log($"AcquireLock Response : channelName:{result.Response.ChannelName},channelType:{result.Response.ChannelType},lockName:{result.Response.LockName}"); + } + } + + // Remove a lock by owner + public async void RemoveLock(string lockName, string owner) + { + IRtmLock rtmLock = signalingEngine.GetLock(); + + // Remove lock by owner asynchronously + var result = await rtmLock.RevokeLockAsync(configData.channelName, RTM_CHANNEL_TYPE.MESSAGE, lockName, owner); + if (result.Status.Error) + { + Debug.LogError($"rtmLock.RevokeLock Status.Reason:{result.Status.Reason}"); + } + else + { + Debug.Log($"RevokeLock Response : channelName:{result.Response.ChannelName},channelType:{result.Response.ChannelType},lockName:{result.Response.LockName}"); + } + } + ``` + * Lock + + + + + ```cpp + void SignalingManagerStorage::setLock(std::string lockName) + { + int ttl = 90; // The lock expiration time ins seconds, value range is [10,300]. + uint64_t requestId; // Output parameter used to identify and process the result + int ret = signalingEngine->getLock()->setLock(channelName.c_str(), channelType, + lockName.c_str(), ttl, requestId); + + if (ret != RTM_ERROR_OK) + { + printf("setLock failed error: %d reason: %s\n", ret, getErrorReason(ret)); + } + } + + void SignalingManagerStorage::acquireLock(std::string lockName) + { + uint64_t requestId; // Output parameter used to identify and process the result + bool retry = false; + int ret = signalingEngine->getLock()->acquireLock(channelName.c_str(), channelType, lockName.c_str(), retry, requestId); + if (ret != RTM_ERROR_OK) + { + printf("acquireLock failed error: %d reason: %s\n", ret, getErrorReason(ret)); + } + } + + void SignalingManagerStorage::releaseLock(std::string lockName) + { + uint64_t requestId; // Output parameter used to identify and process the result + int ret = signalingEngine->getLock()->releaseLock(channelName.c_str(), channelType, lockName.c_str(), requestId); + if (ret != RTM_ERROR_OK) + { + printf("releaseLock failed error: %d reason: %s\n", ret, getErrorReason(ret)); + } + } + + void SignalingManagerStorage::removeLock(std::string lockName) + { + uint64_t requestId; // Output parameter used to identify and process the result + int ret = signalingEngine->getLock()->removeLock(channelName.c_str(), channelType, lockName.c_str(), requestId); + if (ret != RTM_ERROR_OK) + { + printf("removeLock failed error: %d reason: %s\n", ret, getErrorReason(ret)); + } + } + + void SignalingManagerStorage::getLocks(std::string channelName) + { + uint64_t requestId; // Output parameter used to identify and process the result + int ret = signalingEngine->getLock()->getLocks(channelName.c_str(), channelType, requestId); + if (ret != RTM_ERROR_OK) + { + printf("getLocks failed error: %d reason: %s\n", ret, getErrorReason(ret)); + } + } + ``` + + ```cpp + void StorageEventHandler::onSetLockResult(const uint64_t requestId, + const char *channelName, RTM_CHANNEL_TYPE channelType, + const char *lockName, RTM_ERROR_CODE errorCode) + { + if (errorCode != RTM_ERROR_OK) + { + printf("setLock failed error: %d reason: %s\n", errorCode, getErrorReason(errorCode)); + } + else + { + printf("setLock success\n"); + } + } + + void StorageEventHandler::onAcquireLockResult(const uint64_t requestId, const char *channelName, RTM_CHANNEL_TYPE channelType, const char *lockName, RTM_ERROR_CODE errorCode, const char *errorDetails) + { + if (errorCode != RTM_ERROR_OK) + { + printf("acquireLock failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode)); + } + else + { + printf("acquireLock success\n"); + } + } + + void StorageEventHandler::onReleaseLockResult(const uint64_t requestId, const char *channelName, RTM_CHANNEL_TYPE channelType, const char *lockName, RTM_ERROR_CODE errorCode) + { + if (errorCode != RTM_ERROR_OK) + { + printf("releaseLock failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode)); + } + else + { + printf("releaseLock success\n"); + } + } + + void StorageEventHandler::onGetLocksResult(const uint64_t requestId, const char *channelName, RTM_CHANNEL_TYPE channelType, const LockDetail *lockDetailList, const size_t count, RTM_ERROR_CODE errorCode) + { + if (errorCode != RTM_ERROR_OK) + { + printf("getLock failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode)); + } + else + { + printf("getLock success\n"); + for (int i = 0; i < count; i++) + { + printf("lock: %s owner: %s ttl: %d\n", lockDetailList[i].lockName, lockDetailList[i].owner, lockDetailList[i].ttl); + } + } + } + + void StorageEventHandler::onRemoveLockResult(const uint64_t requestId, const char *channelName, RTM_CHANNEL_TYPE channelType, const char *lockName, RTM_ERROR_CODE errorCode) + { + if (errorCode != RTM_ERROR_OK) + { + printf("removeLock failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode)); + } + else + { + printf("removeLock success\n"); + } + } + ``` + * Lock + + diff --git a/assets/code/signaling/storage/set-channel-metadata.mdx b/assets/code/signaling/storage/set-channel-metadata.mdx index d4bea98c2..e993a9d08 100644 --- a/assets/code/signaling/storage/set-channel-metadata.mdx +++ b/assets/code/signaling/storage/set-channel-metadata.mdx @@ -70,7 +70,7 @@ - RtmStorage - createMetadata() - setMetadataItem(_:) - - RtmMetadataItem(key:value:revision:) + - RtmMetadataItem(key:value:revision:) - setMetadata(forChannel:data:options:lock​:) - setMetadata(forChannel:data:options:lock​:completion:) @@ -83,7 +83,7 @@ - RtmStorage - createMetadata() - setMetadataItem(_:) - - RtmMetadataItem(key:value:revision:) + - RtmMetadataItem(key:value:revision:) - setMetadata(forChannel:data:options:lock​:) - setMetadata(forChannel:data:options:lock​:completion:) @@ -124,4 +124,84 @@ * setChannelMetadata + + ```csharp + // Update channel metadata + public async void UpdateChannelMetadata(string channelName, string value, string key, int revision, string lockName) + { + // Configure metadata options + MetadataOptions metadataOptions = new MetadataOptions() + { + recordUserId = true, + recordTs = true + }; + + // Create metadata item + MetadataItem item = new MetadataItem(); + item.authorUserId = configData.uid; + item.value = value; + item.key = key; + item.revision = -1; + + // Create overall metadata structure + RtmMetadata rtmMetadata = new RtmMetadata(); + rtmMetadata.majorRevision = revision; + rtmMetadata.metadataItems = new MetadataItem[1] { item }; + IRtmStorage rtmStorage = signalingEngine.GetStorage(); + + // Set channel metadata asynchronously + var (status, response) = await rtmStorage.SetChannelMetadataAsync(channelName, RTM_CHANNEL_TYPE.MESSAGE, rtmMetadata, metadataOptions, lockName); + if (status.Error) + { + Debug.LogError($"SetChannelMetadata Status.reason:{status.Reason}"); + } + else + { + LDebug.Log($"SetChannelMetadata Response : userId:{response}"); + } + } + ``` + * SetChannelMetadataAsync + + + + ```cpp + // Update channel metadata + public async void UpdateChannelMetadata(string channelName, string value, string key, int revision, string lockName) + { + // Configure metadata options + MetadataOptions metadataOptions = new MetadataOptions() + { + recordUserId = true, + recordTs = true + }; + + // Create metadata item + MetadataItem item = new MetadataItem(); + item.authorUserId = configData.uid; + item.value = value; + item.key = key; + item.revision = -1; + + // Create overall metadata structure + RtmMetadata rtmMetadata = new RtmMetadata(); + rtmMetadata.majorRevision = revision; + rtmMetadata.metadataItems = new MetadataItem[1] { item }; + IRtmStorage rtmStorage = signalingEngine.GetStorage(); + + // Set channel metadata asynchronously + var result = await rtmStorage.SetChannelMetadataAsync(channelName, RTM_CHANNEL_TYPE.MESSAGE, rtmMetadata, metadataOptions, lockName); + if (result.Status.Error) + { + LogError($"SetChannelMetadata Status.reason:{result.Status.Reason}"); + } + else + { + LogInfo($"SetChannelMetadata Response : channelName:{result.Response.ChannelName}"); + } + } + ``` + * setChannelMetadata + + diff --git a/assets/code/signaling/storage/set-user-metadata.mdx b/assets/code/signaling/storage/set-user-metadata.mdx index f29ce7fc9..fd5c6cdc9 100644 --- a/assets/code/signaling/storage/set-user-metadata.mdx +++ b/assets/code/signaling/storage/set-user-metadata.mdx @@ -46,14 +46,14 @@ - createMetadata() - setMetadataItem(_:) - - RtmMetadataItem(key:value:revision:) + - RtmMetadataItem(key:value:revision:) - setMetadata(forUser:data:options:) - setMetadata(forUser:data:options:completion:) - createMetadata() - setMetadataItem(_:) - - RtmMetadataItem(key:value:revision:) + - RtmMetadataItem(key:value:revision:) - setMetadata(forUser:data:options:) - setMetadata(forUser:data:options:completion:) @@ -93,4 +93,66 @@ * setUserMetadata + + ```csharp + // Set user metadata + public async void SetUserMetadata(string uid, string key, string value) + { + // Configure metadata options + MetadataOptions metadataOptions = new MetadataOptions() + { + recordUserId = true, + recordTs = true + }; + + // Create metadata item + MetadataItem items = new MetadataItem(); + items.authorUserId = uid; + items.value = value; + items.key = key; + items.revision = -1; + + // Create overall metadata structure + RtmMetadata rtmMetadata = new RtmMetadata(); + rtmMetadata.majorRevision = -1; + rtmMetadata.metadataItems = new MetadataItem[1] { items }; + + // Get RTM storage instance + IRtmStorage rtmStorage = signalingEngine.GetStorage(); + // Set user metadata asynchronously + var result = await rtmStorage.SetUserMetadataAsync(uid, rtmMetadata, metadataOptions); + if (result.Status.Error) + { + LogError($"SetUserMetadata Status.ErrorCode:{result.Status.ErrorCode}"); + } + else + { + LogInfo($"SetUserMetadata Response : userId:{result.Response.UserId}"); + } + } + ``` + * SetUserMetadataAsync + + + + + ```cpp + void SignalingManagerStorage::setUserMetadata(std::string key, std::string value, int64_t revision) + { + IMetadata *metadata = signalingEngine->getStorage()->createMetadata(); + MetadataOptions options; + MetadataItem item; + item.key = key.c_str(); + item.value = value.c_str(); + item.revision = revision; + item.authorUserId = uid.c_str(); + metadata->setMetadataItem(item); + + uint64_t requestId; // Output parameter used to identify and process the result + int ret = signalingEngine->getStorage()->setUserMetadata( + uid.c_str(), metadata, options, requestId); + } + ``` + * setUserMetadata + diff --git a/assets/code/signaling/storage/subscribe-user-metadata.mdx b/assets/code/signaling/storage/subscribe-user-metadata.mdx index df9faaa26..983c429b5 100644 --- a/assets/code/signaling/storage/subscribe-user-metadata.mdx +++ b/assets/code/signaling/storage/subscribe-user-metadata.mdx @@ -62,4 +62,39 @@ * subscribeUserMetadata + + ```csharp + + // Subscribe to user metadata + public async void SubscribeUserMetadata(string uid) + { + IRtmStorage rtmStorage = signalingEngine.GetStorage(); + // Subscribe to user metadata asynchronously + var result = await rtmStorage.SubscribeUserMetadataAsync(uid); + if (result.Status.Error) + { + LogError($"SubscribeUserMetadata Status.Reason:{result.Status.Reason}"); + } + else + { + LogInfo($"SubscribeUserMetadata Response userId:{result.Response.UserId}"); + } + } + ``` + + + ```cpp + void SignalingManagerStorage::subscribeUserMetadata(std::string userId) + { + uint64_t requestId; // Output parameter used to identify and process the result + int ret = signalingEngine->getStorage()->subscribeUserMetadata(userId.c_str(), requestId); + + if (ret != RTM_ERROR_OK) + { + printf("subscribeUserMetadata failed error is %d reason is %s\n", ret, getErrorReason(ret)); + } + } + ``` + + diff --git a/assets/code/signaling/storage/update-user-metadata.mdx b/assets/code/signaling/storage/update-user-metadata.mdx index 399ee5140..6e5b06440 100644 --- a/assets/code/signaling/storage/update-user-metadata.mdx +++ b/assets/code/signaling/storage/update-user-metadata.mdx @@ -47,14 +47,14 @@ - createMetadata() - setMetadataItem(_:) - - RtmMetadataItem(key:value:revision:) + - RtmMetadataItem(key:value:revision:) - updateMetadata(forUser:data:options:) - updateMetadata(forUser:data:options:completion:) - createMetadata() - setMetadataItem(_:) - - RtmMetadataItem(key:value:revision:) + - RtmMetadataItem(key:value:revision:) - updateMetadata(forUser:data:options:) - updateMetadata(forUser:data:options:completion:) @@ -95,3 +95,65 @@ * updateUserMetadata + + ```csharp + // Update user metadata + public async void UpdateUserMetadata(string uid, string key, string value) + { + // Configure metadata options + MetadataOptions metadataOptions = new MetadataOptions() + { + recordUserId = true, + recordTs = true + }; + + // Create metadata item + MetadataItem items = new MetadataItem(); + items.authorUserId = uid; + items.value = value; + items.key = key; + items.revision = -1; + + // Create overall metadata structure + RtmMetadata rtmMetadata = new RtmMetadata(); + rtmMetadata.majorRevision = -1; + rtmMetadata.metadataItems = new MetadataItem[1] { items }; + + IRtmStorage rtmStorage = signalingEngine.GetStorage(); + // Update user metadata asynchronously + var result = await rtmStorage.UpdateUserMetadataAsync(uid, rtmMetadata, metadataOptions); + if (result.Status.Error) + { + LogError($"UpdateUserMetadata Status.reason:{result.Status.Reason}"); + } + else + { + LogInfo($"UpdateUserMetadata Response ,userId:{result.Response.UserId}"); + } + } + ``` + * UpdateUserMetadataAsync + + + + + ```cpp + void SignalingManagerStorage::updateUserMetadata(std::string key, std::string value, int64_t revision) + { + IMetadata *metadata = signalingEngine->getStorage()->createMetadata(); + MetadataOptions options; + MetadataItem item; + item.key = key.c_str(); + item.value = value.c_str(); + item.revision = revision; + item.authorUserId = uid.c_str(); + metadata->setMetadataItem(item); + + uint64_t requestId; // Output parameter used to identify and process the result + int ret = signalingEngine->getStorage()->updateUserMetadata( + uid.c_str(), metadata, options, requestId); + } + ``` + * updateUserMetadata + + diff --git a/assets/code/signaling/stream-channel/join-leave-channel.mdx b/assets/code/signaling/stream-channel/join-leave-channel.mdx index 3b90dc832..c641006da 100644 --- a/assets/code/signaling/stream-channel/join-leave-channel.mdx +++ b/assets/code/signaling/stream-channel/join-leave-channel.mdx @@ -134,4 +134,99 @@ * createStreamChannel * join * leave - \ No newline at end of file + + + ```csharp + // Join and leave the stream channel + public async void JoinStreamChannel() + { + JoinChannelOptions options = new JoinChannelOptions(); + options.token = configData.rtcToken; + options.withMetadata = false; + options.withPresence = true; + options.withLock = false; + + var result = await signalingChannel.JoinAsync(options); + if (result.Status.Error) + { + isChannelJoined = false; + LogError(string.Format("Join Status.Reason:{0} ", result.Status.Reason)); + } + else + { + string str = string.Format("Join Response: channelName:{0} userId:{1}", + result.Response.ChannelName, result.Response.UserId); + isChannelJoined = true; + LogInfo(str); + } + } + + public async void LeaveStreamChannel() + { + var result = await signalingChannel.LeaveAsync(); + + if (result.Status.Error) + { + LogError(string.Format("StreamChannel.Leave Status.ErrorCode:{0} ", result.Status.ErrorCode)); + } + else + { + string str = string.Format("StreamChannel.Leave Response: channelName:{0} userId:{1}", + result.Response.ChannelName, result.Response.UserId); + isChannelJoined = false; + LogInfo(str); + } + + } + ``` + * CreateStreamChannel + * JoinAsync + * LeaveAsync + + + ```cpp + void SignalingManagerStreamChannel::joinStreamChannel(std::string channelName) + { + // Create a stream channel + channelType = RTM_CHANNEL_TYPE_STREAM; + + streamChannel = signalingEngine->createStreamChannel(channelName.c_str()); + if (streamChannel == nullptr) + { + printf("createStreamChannel failed\n"); + return; + } + else + { + printf("createStreamChannel success\n"); + this->channelName = channelName; + } + + // Join the stream channel + std::string rtcToken = fetchRTCToken(channelName); // config["rtcToken"]; + JoinChannelOptions options; + options.token = rtcToken.c_str(); + options.withLock = true; + options.withMetadata = true; + options.withPresence = true; + + uint64_t requestId; // Output parameter used to identify and process the result + int ret = streamChannel->join(options, requestId); + std::cout << "setLock returned: " << ret << std::endl; + } + + void SignalingManagerStreamChannel::leaveStreamChannel(std::string channelName) + { + uint64_t requestId; // Output parameter used to identify and process the result + int ret = streamChannel->leave(requestId); + + if (ret != RTM_ERROR_OK) + { + printf("leave rtm channel failed error: %d reason: %s\n", ret, getErrorReason(ret)); + } + } + ``` + * createStreamChannel + * join + * leave + diff --git a/assets/code/signaling/stream-channel/join-leave-topic.mdx b/assets/code/signaling/stream-channel/join-leave-topic.mdx index e0823555c..082566b1b 100644 --- a/assets/code/signaling/stream-channel/join-leave-topic.mdx +++ b/assets/code/signaling/stream-channel/join-leave-topic.mdx @@ -85,4 +85,96 @@ * joinTopic * leaveTopic + + + ```csharp + public async void JoinTopic(string topic) + { + JoinTopicOptions options = new JoinTopicOptions() + { + qos = RTM_MESSAGE_QOS.ORDERED, + priority = RTM_MESSAGE_PRIORITY.NORMAL, + meta = "My topic", + syncWithMedia = true + }; + var (status, response) = await signalingChannel.JoinTopicAsync(topic, options); + if (status.Error) + { + Debug.LogError(string.Format("signalingChannel.JoinTopic Status.Reason:{0} ", status.Reason)); + } + else + { + string str = string.Format("signalingChannel.JoinTopic Response: channelName:{0} userId:{1} topic:{2} meta:{3}", + response.ChannelName, response.UserId, response.Topic, response.Meta); + isTopicJoined = false; + Debug.Log(str); + } + } + + public async void LeaveTopic(string topic) + { + var (status, response) = await signalingChannel.LeaveTopicAsync(topic); + + if (status.Error) + { + LogError(string.Format("signalingChannel.LeaveTopic Status.Reason:{0} ", status.Reason)); + } + else + { + string str = string.Format("signalingChannel.LeaveTopic Response: channelName:{0} userId:{1} topic:{2} meta:{3}", + response.ChannelName, response.UserId, response.Topic, response.Meta); + isTopicJoined = false; + Debug.Log(str); + } + } + ``` + + * JoinTopicAsync + * LeaveTopicAsync + + + + ```cpp + public async void JoinTopic(string topic) + { + JoinTopicOptions options = new JoinTopicOptions() + { + qos = RTM_MESSAGE_QOS.ORDERED, + priority = RTM_MESSAGE_PRIORITY.NORMAL, + meta = "My topic", + syncWithMedia = true + }; + var result = await signalingChannel.JoinTopicAsync(topic, options); + if (result.Status.Error) + { + LogError(string.Format("signalingChannel.JoinTopic Status.Reason:{0} ", result.Status.Reason)); + } + else + { + string str = string.Format("signalingChannel.JoinTopic Response: channelName:{0} userId:{1} topic:{2} meta:{3}", + result.Response.ChannelName, result.Response.UserId, result.Response.Topic, result.Response.Meta); + isTopicJoined = false; + LogInfo(str); + } + } + + public async void LeaveTopic(string topic) + { + var result = await signalingChannel.LeaveTopicAsync(topic); + + if (result.Status.Error) + { + LogError(string.Format("signalingChannel.LeaveTopic Status.Reason:{0} ", result.Status.Reason)); + } + else + { + string str = string.Format("signalingChannel.LeaveTopic Response: channelName:{0} userId:{1} topic:{2} meta:{3}", + result.Response.ChannelName, result.Response.UserId, result.Response.Topic, result.Response.Meta); + isTopicJoined = false; + LogInfo(str); + } + } + ``` + * joinTopic + * leaveTopic \ No newline at end of file diff --git a/assets/code/signaling/stream-channel/publish-message.mdx b/assets/code/signaling/stream-channel/publish-message.mdx index c95f548a4..a65159b45 100644 --- a/assets/code/signaling/stream-channel/publish-message.mdx +++ b/assets/code/signaling/stream-channel/publish-message.mdx @@ -31,12 +31,12 @@ - publishTopicMessage(message:inTopic:with:) - - publishTopicMessage(message:inTopic:with:completion:) + - publishTopicMessage(_:in:with:completion:) - RtmPublishOptions - publishTopicMessage(message:inTopic:with:) - - publishTopicMessage(message:inTopic:with:completion:) + - publishTopicMessage(_:in:with:completion:) - RtmPublishOptions @@ -59,4 +59,44 @@ ``` * publishTopicMessage - \ No newline at end of file + + + ```csharp + + public async void SendTopicMessage(string msg, string topic) + { + TopicMessageOptions options = new TopicMessageOptions(); + options.customType = "byte"; + var result = await signalingChannel.PublishTopicMessageAsync(topic, Encoding.UTF8.GetBytes(msg), options); + if(result.Status.Error) + { + LogError(string.Format("signalingChannel.PublishTopicMessageAsync Status.Reason:{0} ", result.Status.Reason)); + } + else + { + msg = "Topic name:" + topic + "message: " + msg; + signalingUI.AddTextToDisplay(msg, Color.blue, TextAlignmentOptions.BaselineRight); + LogInfo("StreamChannel.PublishTopicMessage ret:" + result.Status.ErrorCode); + } + } + ``` + * PublishTopicMessageAsync + + + ```cpp + void SignalingManagerStreamChannel::publishTopicMessage(std::string topic, std::string message) + { + const TopicMessageOptions topicMessageOptions; + + int ret = streamChannel->publishTopicMessage(topic.c_str(), message.c_str(), + message.length(), topicMessageOptions); + if (ret != RTM_ERROR_OK) + { + printf("publishTopicMessage failed error: %d reason: %s\n", ret, getErrorReason(ret)); + } + } + ``` + + * publishTopicMessage + + diff --git a/assets/code/signaling/stream-channel/subscribe-topic.mdx b/assets/code/signaling/stream-channel/subscribe-topic.mdx index 54404c589..ac6d4d323 100644 --- a/assets/code/signaling/stream-channel/subscribe-topic.mdx +++ b/assets/code/signaling/stream-channel/subscribe-topic.mdx @@ -47,4 +47,50 @@ * subscribeTopic * unsubscribeTopic + + + ```csharp + // Subscribe to a topic to receive messages. + public async void SubscribeTopic(string topic) + { + TopicOptions options = new TopicOptions(); + var result = await signalingChannel.SubscribeTopicAsync(topic, options); + if (result.Status.Error) + { + Debug.LogError(string.Format("signalingChannel.SubscribeTopicAsync Status.Reason:{0} ", result.Status.Reason)); + } + else + { + string str = string.Format("signalingChannel.SubscribeTopicAsync Response: channelName:{0} userId:{1} topic:{2}", + result.Response.ChannelName, result.Response.UserId, result.Response.Topic); + Debug.Log(str); + } + } + ``` + * SubscribeTopicAsync + + + + + ```cpp + void SignalingManagerStreamChannel::subscribeTopic(std::string topicName, std::string user) + { + uint64_t requestId; // Output parameter used to identify and process the result + TopicOptions topicOptions; + + // Initialize an empty userList + const char *userCStr = user.c_str(); + + topicOptions.userCount = 1; + topicOptions.users = &userCStr; + + int ret = streamChannel->subscribeTopic(topicName.c_str(), topicOptions, requestId); + if (ret != RTM_ERROR_OK) + { + printf("subscribeTopic failed error: %d reason: %s\n", ret, getErrorReason(ret)); + } + } + ``` + + * subscribeTopic \ No newline at end of file diff --git a/assets/code/video-sdk/ai-noise-suppression/configure-engine.mdx b/assets/code/video-sdk/ai-noise-suppression/configure-engine.mdx new file mode 100644 index 000000000..91ce71e72 --- /dev/null +++ b/assets/code/video-sdk/ai-noise-suppression/configure-engine.mdx @@ -0,0 +1,18 @@ + + ```typescript + export function AINoiseReduction() { + const agoraEngine = useRTCClient(AgoraRTC.createClient({ codec: "vp8", mode: "rtc" })); + + return ( +
+

AI Noise Suppression

+ + + + + +
+ ); + } + ``` +
\ No newline at end of file diff --git a/assets/code/video-sdk/ai-noise-suppression/configure-extension.mdx b/assets/code/video-sdk/ai-noise-suppression/configure-extension.mdx new file mode 100644 index 000000000..d5a504394 --- /dev/null +++ b/assets/code/video-sdk/ai-noise-suppression/configure-extension.mdx @@ -0,0 +1,41 @@ + + ```typescript + const extension = useRef(new AIDenoiserExtension({assetsPath:'./node_modules/agora-extension-ai-denoiser/external'})); + const processor = useRef(); + + useEffect(() => { + const initializeAIDenoiserProcessor = async () => { + AgoraRTC.registerExtensions([extension.current]); + if (!extension.current.checkCompatibility()) { + console.error("Does not support AI Denoiser!"); + return; + } + + if (agoraContext.localMicrophoneTrack) + { + console.log("Initializing an ai noise processor..."); + try { + processor.current = extension.current.createProcessor(); + agoraContext.localMicrophoneTrack.pipe(processor.current).pipe(agoraContext.localMicrophoneTrack.processorDestination); + await processor.current.enable(); + } catch (error) { + console.error("Error applying noise reduction:", error); + } + } + }; + void initializeAIDenoiserProcessor(); + + return () => { + const disableAIDenoiser = async () => { + processor.current?.unpipe(); + agoraContext.localMicrophoneTrack.unpipe(); + await processor.current?.disable(); + }; + void disableAIDenoiser(); + }; + }, [agoraContext.localMicrophoneTrack]); + ``` + * pipe + * unpipe + + \ No newline at end of file diff --git a/assets/code/video-sdk/ai-noise-suppression/enable-denoiser.mdx b/assets/code/video-sdk/ai-noise-suppression/enable-denoiser.mdx new file mode 100644 index 000000000..f28d97f91 --- /dev/null +++ b/assets/code/video-sdk/ai-noise-suppression/enable-denoiser.mdx @@ -0,0 +1,74 @@ + + ```javascript + // Create an AIDenoiserExtension instance, and pass in the host URL of the Wasm files + const denoiser = new AIDenoiserExtension({ assetsPath: "/node_modules/agora-extension-ai-denoiser/external/" }); + // Check compatibility + if (!denoiser.checkCompatibility()) { + // The extension might not be supported in the current browser. You can stop executing further code logic + console.error("Does not support AI Denoiser!"); + } + // Register the extension + AgoraRTC.registerExtensions([denoiser]); + // (Optional) Listen for the callback reporting that the Wasm files fail to load + denoiser.onloaderror = (e) => { + // If the Wasm files fail to load, you can disable the plugin, for example: + // openDenoiserButton.enabled = false; + console.log(e); + }; + + // Create a processor + const processor = denoiser.createProcessor(); + + // Inject the extension to the audio processing pipeline + channelParameters.localAudioTrack + .pipe(processor) + .pipe(channelParameters.localAudioTrack.processorDestination); + + await processor.enable(); + ``` + - [createProcessor](#createprocessor) + - [enable](#enable) + + + ```kotlin + override fun setupAgoraEngine(): Boolean { + val result = super.setupAgoraEngine() + + // Enable AI noise suppression + val mode = 2 + // Choose a noise suppression mode from the following: + // 0: (Default) Balanced noise reduction mode + // 1: Aggressive mode + // 2: Aggressive mode with low latency + agoraEngine!!.setAINSMode(true, mode) + + return result + } + ``` + - setAINSMode + + + + ```swift + func setNoiseSuppression(_ enable: Bool, mode: AUDIO_AINS_MODE) -> Int32 { + self.agoraEngine.setAINSMode(enable, mode: mode) + } + ``` + + - setAINSMode(_:mode:) + - AUDIO_AINS_MODE + + + - setAINSMode(_:mode:) + - AUDIO_AINS_MODE + + + + + Enable AI noise suppression when you configure the engine. + + ```csharp + agoraEngine.SetAINSMode(true, AUDIO_AINS_MODE.AINS_MODE_AGGRESSIVE); + ``` + * setAINSMode + \ No newline at end of file diff --git a/assets/code/video-sdk/ai-noise-suppression/import-library.mdx b/assets/code/video-sdk/ai-noise-suppression/import-library.mdx new file mode 100644 index 000000000..6f4a1c2e5 --- /dev/null +++ b/assets/code/video-sdk/ai-noise-suppression/import-library.mdx @@ -0,0 +1,23 @@ + +```javascript +import AgoraManager from "../agora_manager/agora_manager.js"; +import AgoraRTC from "agora-rtc-sdk-ng"; +import { AIDenoiserExtension } from "agora-extension-ai-denoiser"; +``` + + +```typescript +import AgoraRTC from "agora-rtc-sdk-ng"; +import { useRTCClient, AgoraRTCProvider } from "agora-rtc-react"; +import { useEffect, useRef, useState } from "react"; +import AuthenticationWorkflowManager from "../authentication-workflow/authenticationWorkflowManager"; +import {AIDenoiserExtension, AIDenoiserProcessorLevel, AIDenoiserProcessorMode, IAIDenoiserProcessor} from "agora-extension-ai-denoiser"; +import { useConnectionState } from 'agora-rtc-react'; +import { useAgoraContext } from "../agora-manager/agoraManager"; +``` + + +```csharp +using Agora.Rtc; +``` + diff --git a/assets/code/video-sdk/ai-noise-suppression/import-plugin.mdx b/assets/code/video-sdk/ai-noise-suppression/import-plugin.mdx new file mode 100644 index 000000000..a6385d0ec --- /dev/null +++ b/assets/code/video-sdk/ai-noise-suppression/import-plugin.mdx @@ -0,0 +1,25 @@ + + To enable , you must import the plugin to your target. + + * **Swift Package Manager** + + Add the product "AINS" to your app target. This is part of the AgoraRtcEngine Swift Package. + + * **CocoaPods** + + Include "AINS" in the subspecs in your Podfile: + + ```rb + target 'Your App' do + pod 'AgoraRtcEngine_iOS', '~> 4.2', :subspecs => ['RtcBasic', 'AINS'] + end + ``` + + + ```rb + target 'Your App' do + pod 'AgoraRtcEngine_macOS', '~> 4.2', :subspecs => ['RtcBasic', 'AINS'] + end + ``` + + \ No newline at end of file diff --git a/assets/code/video-sdk/ai-noise-suppression/set-noise-reduction-mode.mdx b/assets/code/video-sdk/ai-noise-suppression/set-noise-reduction-mode.mdx new file mode 100644 index 000000000..5be7929b3 --- /dev/null +++ b/assets/code/video-sdk/ai-noise-suppression/set-noise-reduction-mode.mdx @@ -0,0 +1,34 @@ + +```typescript +const changeNoiseReductionMode = (selectedOption: string) => { + if (!processor.current) { + console.error("AI noise reduction processor not initialized"); + return; + } + if(selectedOption === "STATIONARY_NS") + { + processor.current.setMode(AIDenoiserProcessorMode.STATIONARY_NS) + .then(() => + { + console.log("Mode set to:", selectedOption); + }) + .catch((error) => + { + console.log(error); + }); + } + else + { + processor.current.setMode(AIDenoiserProcessorMode.NSNG) + .then(() => + { + console.log("Mode set to:", selectedOption); + }) + .catch((error) => + { + console.log(error); + }); + } +} +``` + \ No newline at end of file diff --git a/assets/code/video-sdk/ai-noise-suppression/set-reduction-level.mdx b/assets/code/video-sdk/ai-noise-suppression/set-reduction-level.mdx new file mode 100644 index 000000000..45720af9d --- /dev/null +++ b/assets/code/video-sdk/ai-noise-suppression/set-reduction-level.mdx @@ -0,0 +1,34 @@ + +```typescript +const changeNoiseReductionLevel = (selectedOption: string) => { + if (!processor.current) { + console.error("AI noise reduction processor not initialized"); + return; + } + if(selectedOption === "aggressive") + { + processor.current.setLevel(AIDenoiserProcessorLevel.AGGRESSIVE) + .then(() => + { + console.log("Level set to:", selectedOption); + }) + .catch((error) => + { + console.log(error); + }); + } + else + { + processor.current.setLevel(AIDenoiserProcessorLevel.SOFT) + .then(() => + { + console.log("Level set to:", selectedOption); + }) + .catch((error) => + { + console.log(error); + }); + } +} +``` + \ No newline at end of file diff --git a/assets/code/video-sdk/ai-noise-suppression/setup-logging.mdx b/assets/code/video-sdk/ai-noise-suppression/setup-logging.mdx new file mode 100644 index 000000000..0b95a8006 --- /dev/null +++ b/assets/code/video-sdk/ai-noise-suppression/setup-logging.mdx @@ -0,0 +1,21 @@ + + ```javascript + // Setup logging + processor.ondump = (blob, name) => { + // Dump the audio data to a local folder in PCM format + const objectURL = URL.createObjectURL(blob); + const tag = document.createElement("a"); + tag.download = name; + tag.href = objectURL; + tag.click(); + setTimeout(() => {URL.revokeObjectURL(objectURL);}, 0); + } + + processor.ondumpend = () => { + console.log("dump ended!!"); + } + + processor.dump(); + ``` + - [ondump](#ondump) + diff --git a/assets/code/video-sdk/audio-voice-effects/apply-voice-effects.mdx b/assets/code/video-sdk/audio-voice-effects/apply-voice-effects.mdx new file mode 100644 index 000000000..63d29c1a9 --- /dev/null +++ b/assets/code/video-sdk/audio-voice-effects/apply-voice-effects.mdx @@ -0,0 +1,129 @@ + + ```kotlin + fun applyVoiceBeautifierPreset(beautifier: Int) { + // Use a preset value from Constants. For example, Constants.CHAT_BEAUTIFIER_MAGNETIC + agoraEngine?.setVoiceBeautifierPreset(beautifier); + } + + fun applyAudioEffectPreset(preset: Int) { + // Use a preset value from Constants. For example, Constants.VOICE_CHANGER_EFFECT_HULK + agoraEngine?.setAudioEffectPreset(preset) + } + + fun applyVoiceConversionPreset(preset: Int) { + // Use a preset value from Constants. For example, Constants.VOICE_CHANGER_CARTOON + agoraEngine?.setVoiceConversionPreset(preset) + } + + fun applyLocalVoiceFormant(preset: Double) { + // The value range is [-1.0, 1.0]. The default value is 0.0, + agoraEngine?.setLocalVoiceFormant(preset) + } + + fun setVoiceEqualization(bandFrequency: AUDIO_EQUALIZATION_BAND_FREQUENCY, bandGain: Int) { + // Set local voice equalization. + // The first parameter sets the band frequency. The value ranges between 0 and 9. + // Each value represents the center frequency of the band: 31, 62, 125, 250, 500, 1k, 2k, 4k, 8k, and 16k Hz. + // The second parameter sets the gain of each band. The value ranges between -15 and 15 dB. + // The default value is 0. + agoraEngine?.setLocalVoiceEqualization(bandFrequency, bandGain) + } + + fun setVoicePitch(value: Double) { + // The value range is [0.5,2.0] default value is 1.0 + agoraEngine?.setLocalVoicePitch(value) + } + + ``` + - setLocalVoiceEqualization + + - setAudioEffectPreset + + - setAudioEffectParameters + + - setVoiceBeautifierPreset + + - setVoiceConversionPreset + + - setVoiceBeautifierParameters + + - setLocalVoiceReverb + + - setLocalVoiceReverbPreset + + - setLocalVoicePitch + + + ```swift + func applyVoiceBeautifierPreset(beautifier: AgoraVoiceBeautifierPreset) { + // Use a preset value from Constants. For example, Constants.CHAT_BEAUTIFIER_MAGNETIC + agoraEngine.setVoiceBeautifierPreset(beautifier) + } + + func applyAudioEffectPreset(preset: AgoraAudioEffectPreset) { + // Use a preset value from Constants. For example, Constants.VOICE_CHANGER_EFFECT_HULK + agoraEngine.setAudioEffectPreset(preset) + } + + func applyVoiceConversionPreset(preset: AgoraVoiceConversionPreset) { + // Use a preset value from Constants. For example, Constants.VOICE_CHANGER_CARTOON + agoraEngine.setVoiceConversionPreset(preset) + } + + func applyLocalVoiceFormant(preset: Double) { + // The value range is [-1.0, 1.0]. The default value is 0.0, + agoraEngine.setLocalVoiceFormant(preset) + } + + func setVoiceEqualization(bandFrequency: AgoraAudioEqualizationBandFrequency, bandGain: Int) { + // Set local voice equalization. + // The first parameter sets the band frequency. Ranges from 0 to 9. + // Each value represents the center frequency of the band: + // 31, 62, 125, 250, 500, 1k, 2k, 4k, 8k, and 16k Hz. + // The second parameter sets the gain of each band. Ranges from -15 to 15 dB. + // The default value is 0. + agoraEngine.setLocalVoiceEqualizationOf(bandFrequency, withGain: bandGain) + } + + func setVoicePitch(value: Double) { + // The value range is [0.5,2.0] default value is 1.0 + agoraEngine.setLocalVoicePitch(value) + } + ``` + + + - setVoiceBeautifierPreset(_:) + - setAudioEffectPreset(_:) + - setVoiceConversionPreset(_:) + - setLocalVoiceFormant(_:) + - setLocalVoiceEqualizationOf(_:withGain:) + - setLocalVoicePitch(_:) + + + - setVoiceBeautifierPreset(_:) + - setAudioEffectPreset(_:) + - setVoiceConversionPreset(_:) + - setLocalVoiceFormant(_:) + - setLocalVoiceEqualizationOf(_:withGain:) + - setLocalVoicePitch(_:) + + + + ```csharp + // Method to apply voice effects + public void ApplyVoiceEffect(VOICE_BEAUTIFIER_PRESET effect) + { + agoraEngine.SetVoiceBeautifierPreset(effect); + } + ``` + + - SetVoiceBeautifierPreset + - SetAudioEffectPreset + - SetVoiceBeautifierPreset + + + - SetVoiceBeautifierPreset + - SetAudioEffectPreset + - SetVoiceBeautifierPreset + + \ No newline at end of file diff --git a/assets/code/video-sdk/audio-voice-effects/swift/configure-buttons.mdx b/assets/code/video-sdk/audio-voice-effects/configure-buttons.mdx similarity index 99% rename from assets/code/video-sdk/audio-voice-effects/swift/configure-buttons.mdx rename to assets/code/video-sdk/audio-voice-effects/configure-buttons.mdx index 399526d00..24d5b9034 100644 --- a/assets/code/video-sdk/audio-voice-effects/swift/configure-buttons.mdx +++ b/assets/code/video-sdk/audio-voice-effects/configure-buttons.mdx @@ -1,5 +1,5 @@ -``` swift +```swift // Button to start audio mixing audioMixingBtn = NSButton() audioMixingBtn.frame = CGRect(x: 230, y:240, width:80, height:20) @@ -27,7 +27,7 @@ self.view.addSubview(applyVoiceEffectBtn) ``` -``` swift +```swift // Button to start audio mixing audioMixingBtn = UIButton(type: .system) audioMixingBtn.frame = CGRect(x: 60, y:500, width:250, height:50) diff --git a/assets/code/video-sdk/audio-voice-effects/configure-engine.mdx b/assets/code/video-sdk/audio-voice-effects/configure-engine.mdx new file mode 100644 index 000000000..2c1149fbd --- /dev/null +++ b/assets/code/video-sdk/audio-voice-effects/configure-engine.mdx @@ -0,0 +1,69 @@ + +```csharp + // Method to set up the Agora engine + public override void SetupAgoraEngine() + { + base.SetupAgoraEngine(); + + // Pre-load sound effects to improve performance + agoraEngine.PreloadEffect(soundEffectId, configData.soundEffectFileURL); + + // Specify the audio scenario and audio profile + agoraEngine.SetAudioProfile(AUDIO_PROFILE_TYPE.AUDIO_PROFILE_DEFAULT, AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_CHATROOM); + + // Initialize event handling for Agora + agoraEngine.InitEventHandler(new AudioVoiceEffectEventHandler(this)); + +#if (UNITY_ANDROID || UNITY_IOS) + agoraEngine.SetDefaultAudioRouteToSpeakerphone(!enableSpeakerPhone); // Disables the default audio route. + agoraEngine.SetEnableSpeakerphone(enableSpeakerPhone); // Enables or disables the speakerphone temporarily. +#endif + } +``` +The `SetDefaultAudioRouteToSpeakerphone` and `SetEnableSpeakerphone` methods applies to Android and iOS only. + +For more details, see the following: + - PreloadEffect + - SetAudioProfile + - SetDefaultAudioRouteToSpeakerphone + - SetEnableSpeakerphone + + + + ```typescript + function AudioAndVoiceEffects() { + const agoraEngine = useRTCClient(AgoraRTC.createClient({ codec: "vp8", mode: config.selectedProduct })); + + return ( +
+

Audio and voice effects

+ + + + + +
+ ); + } + ``` + - useRTCClient + - AgoraRTCProvider +
+ + ```swift + override func setupEngine() -> AgoraRtcEngineKit { + let eng = super.setupEngine() + eng.setAudioProfile(.musicHighQualityStereo) + eng.setAudioScenario(.gameStreaming) + return eng + } + ``` + + - setAudioProfile(_:) + - setAudioScenario(_:) + + + - setAudioProfile(_:) + - setAudioScenario(_:) + + \ No newline at end of file diff --git a/assets/code/video-sdk/audio-voice-effects/create-ui.mdx b/assets/code/video-sdk/audio-voice-effects/create-ui.mdx new file mode 100644 index 000000000..057c1356a --- /dev/null +++ b/assets/code/video-sdk/audio-voice-effects/create-ui.mdx @@ -0,0 +1,51 @@ + +```swift +var audioMixingBtn: NSButton! +var playAudioEffectBtn: NSButton! +var applyVoiceEffectBtn: NSButton! +``` + + +```swift +var audioMixingBtn: UIButton! +var playAudioEffectBtn: UIButton! +var applyVoiceEffectBtn: UIButton! +var speakerphoneSwitch: UISwitch! +``` + + +```typescript + return ( +
+
+ + +

+ +

+ {showDropdown && ( +
+ + +
+ )} + {isAudioMixing && audioFileTrack && } +
+
+ ); +``` +
\ No newline at end of file diff --git a/assets/code/video-sdk/audio-voice-effects/event-handler.mdx b/assets/code/video-sdk/audio-voice-effects/event-handler.mdx new file mode 100644 index 000000000..8c7de4faa --- /dev/null +++ b/assets/code/video-sdk/audio-voice-effects/event-handler.mdx @@ -0,0 +1,98 @@ + + ```kotlin + override val iRtcEngineEventHandler: IRtcEngineEventHandler + get() = object : IRtcEngineEventHandler() { + + // Occurs when the audio effect playback finishes. + override fun onAudioEffectFinished(soundId: Int) { + super.onAudioEffectFinished(soundId) + sendMessage("Audio effect finished") + audioEffectManager!!.stopEffect(soundId) + // Notify the UI + val eventArgs: Map = mapOf("soundId" to soundId) + mListener?.onEngineEvent("onAudioEffectFinished", eventArgs) + } + } + ``` + - onAudioEffectFinished + + + +```csharp +// Event handler class to handle the events raised by Agora's RtcEngine instance +internal class AudioVoiceEffectEventHandler : UserEventHandler +{ + private AudioVoiceEffectsManager audioVoiceEffectsManager; + + internal AudioVoiceEffectEventHandler(AudioVoiceEffectsManager videoSample) : base(videoSample) + { + audioVoiceEffectsManager = videoSample; + } + + // Occurs when the audio effect playback finishes + public override void OnAudioEffectFinished(int soundId) + { + // Handle the event, stop the audio effect, and reset its status + Debug.Log("Audio effect finished"); + audioVoiceEffectsManager.isEffectFinished = true; + audioVoiceEffectsManager.StopAudioMixing(); + } + + // Occurs when you start audio mixing, with different states + public override void OnAudioMixingStateChanged(AUDIO_MIXING_STATE_TYPE state, AUDIO_MIXING_REASON_TYPE reason) + { + audioVoiceEffectsManager.audioMixingState = state; + // Handle audio mixing state changes, such as failure, pause, play, or stop + if (state == AUDIO_MIXING_STATE_TYPE.AUDIO_MIXING_STATE_FAILED) + { + Debug.Log("Audio mixing failed: " + reason); + } + else if (state == AUDIO_MIXING_STATE_TYPE.AUDIO_MIXING_STATE_PAUSED) + { + Debug.Log("Audio mixing paused : " + reason); + } + else if (state == AUDIO_MIXING_STATE_TYPE.AUDIO_MIXING_STATE_PLAYING) + { + Debug.Log("Audio mixing started: " + reason); + } + else if (state == AUDIO_MIXING_STATE_TYPE.AUDIO_MIXING_STATE_STOPPED) + { + Debug.Log("Audio mixing stopped: " + reason); + } + } + + // Occurs when the audio route changes + public override void OnAudioRoutingChanged(int routing) + { + if (routing != (int)AudioRoute.ROUTE_DEFAULT) + { + Debug.Log("Audio route changed"); + } + } +} +``` + + - OnAudioEffectFinished + - OnAudioMixingStateChanged + - OnAudioRoutingChanged + + + - OnAudioEffectFinished + - OnAudioMixingStateChanged + - OnAudioRoutingChanged + + + + ```swift + func rtcEngineDidAudioEffectFinish(_ engine: AgoraRtcEngineKit, soundId: Int32) { + // Occurs when the audio effect playback finishes. + } + ``` + + + - rtcEngineDidAudioEffectFinish(_:soundId:) + + + - rtcEngineDidAudioEffectFinish(_:soundId:) + + diff --git a/assets/code/video-sdk/audio-voice-effects/import-library.mdx b/assets/code/video-sdk/audio-voice-effects/import-library.mdx new file mode 100644 index 000000000..a29a7ed3c --- /dev/null +++ b/assets/code/video-sdk/audio-voice-effects/import-library.mdx @@ -0,0 +1,26 @@ + + ```kotlin + import io.agora.rtc2.Constants + import io.agora.rtc2.Constants.AUDIO_EQUALIZATION_BAND_FREQUENCY + import io.agora.rtc2.IAudioEffectManager + import io.agora.rtc2.IRtcEngineEventHandler + ``` + + +```csharp +using Agora.Rtc; +``` + + + ```typescript + import { AgoraRTCProvider, useRTCClient, usePublish, useConnectionState } from "agora-rtc-react"; + import AgoraRTC, {IBufferSourceAudioTrack} from "agora-rtc-sdk-ng"; + import AuthenticationWorkflowManager from "../authentication-workflow/authenticationWorkflowManager"; + import config from "../agora-manager/config"; + ``` + + + ```swift + import AgoraRtcKit + ``` + \ No newline at end of file diff --git a/assets/code/video-sdk/audio-voice-effects/pause-play-resume.mdx b/assets/code/video-sdk/audio-voice-effects/pause-play-resume.mdx new file mode 100644 index 000000000..6d3dbcca6 --- /dev/null +++ b/assets/code/video-sdk/audio-voice-effects/pause-play-resume.mdx @@ -0,0 +1,105 @@ + + ```kotlin + fun playEffect(soundEffectId: Int, soundEffectFilePath: String) { + audioEffectManager!!.playEffect( + soundEffectId, // The ID of the sound effect file. + soundEffectFilePath, // The path of the sound effect file. + 0, 1.0, // The pitch of the audio effect. 1 represents the original pitch. + 0.0, 100.0, // The volume of the audio effect. 100 represents the original volume. + true, // Whether to publish the audio effect to remote users. + 0 // The playback starting position of the audio effect file in ms. + ) + } + + fun pauseEffect(soundEffectId: Int) { + audioEffectManager!!.pauseEffect(soundEffectId) + } + + fun resumeEffect(soundEffectId: Int) { + audioEffectManager!!.resumeEffect(soundEffectId) + } + ``` + - playEffect + - pauseEffect + - resumeEffect + + + ```csharp + // Method to play the sound effect + public void PlaySoundEffect() + { + agoraEngine.PlayEffect( + soundEffectId, // The ID of the sound effect file. + configData.soundEffectFileURL, // The path of the sound effect file. + 0, // The number of sound effect loops. -1 means an infinite loop. 0 means once. + 1, // The pitch of the audio effect. 1 represents the original pitch. + 0.0, // The spatial position of the audio effect. 0.0 represents that the audio effect plays in the front. + 100, // The volume of the audio effect. 100 represents the original volume. + true,// Whether to publish the audio effect to remote users. + 0 // The playback starting position of the audio effect file in ms. + ); + } + + // Pause the sound effect + public void PauseSoundEffect() + { + agoraEngine.PauseEffect(soundEffectId); + } + + // Resume the sound effect + public void ResumeSoundEffect() + { + agoraEngine.ResumeEffect(soundEffectId); + } + // Method to get the current voice effect state + public bool GetSoundEffectState() + { + return isEffectFinished; + } + ``` + + - PlayEffect + - PauseEffect + - ResumeEffect + + + - PlayEffect + - PauseEffect + - ResumeEffect + + + + ```swift + func playEffect(soundEffectId: Int32, effectFilePath: String) { + agoraEngine.playEffect( + soundEffectId, // The ID of the sound effect file. + filePath: effectFilePath, // The path of the sound effect file. + loopCount: 0, + pitch: 1.0, // The pitch of the audio effect. 1 = original pitch. + pan: 0.0, // The spatial position of the audio effect (-1 to 1) + gain: 100, // The volume of the audio effect. 100 = original volume. + publish: true, // Whether to publish the audio effect to remote users. + startPos: 0 // The playback starting position (in ms). + ) + } + + func pauseEffect(soundEffectId: Int32) { + agoraEngine.pauseEffect(soundEffectId) + } + + func resumeEffect(soundEffectId: Int32) { + agoraEngine.resumeEffect(soundEffectId) + } + ``` + + + - playEffect(_:filePath:loopCount:pitch:pan:gain:publish:startPos:) + - pauseEffect(_:) + - resumeEffect(_:) + + + - playEffect(_:filePath:loopCount:pitch:pan:gain:publish:startPos:) + - pauseEffect(_:) + - resumeEffect(_:) + + \ No newline at end of file diff --git a/assets/code/video-sdk/audio-voice-effects/preload-effect.mdx b/assets/code/video-sdk/audio-voice-effects/preload-effect.mdx new file mode 100644 index 000000000..d9af2c9db --- /dev/null +++ b/assets/code/video-sdk/audio-voice-effects/preload-effect.mdx @@ -0,0 +1,47 @@ + + ```kotlin + if (audioEffectManager == null) { + // Set up the audio effects manager + audioEffectManager = agoraEngine?.audioEffectManager + // Pre-load sound effects to improve performance + audioEffectManager?.preloadEffect(soundEffectId, soundEffectFilePath) + } + ``` + - IAudioEffectManager + - preloadEffect + + + ```csharp + public override void SetupAgoraEngine() + { + base.SetupAgoraEngine(); + + // Pre-load sound effects to improve performance + agoraEngine.PreloadEffect(soundEffectId, configData.soundEffectFileURL); + + // Initialize event handling for Agora + agoraEngine.InitEventHandler(new AudioVoiceEffectEventHandler(this)); + } + ``` + + - PreloadEffect + + + - PreloadEffect + + + + ```swift + func preloadEffect(soundEffectId: Int32, effectFilePath: String) { + // Pre-load sound effects to improve performance + agoraEngine.preloadEffect(soundEffectId, filePath: effectFilePath) + } + ``` + + + - preloadEffect(_:filePath:) + + + - preloadEffect(_:filePath:) + + diff --git a/assets/code/video-sdk/audio-voice-effects/set-audio-profile.mdx b/assets/code/video-sdk/audio-voice-effects/set-audio-profile.mdx new file mode 100644 index 000000000..d1cccb0cb --- /dev/null +++ b/assets/code/video-sdk/audio-voice-effects/set-audio-profile.mdx @@ -0,0 +1,41 @@ + + ```kotlin + override fun setupAgoraEngine(): Boolean { + val result = super.setupAgoraEngine() + + // Set the audio scenario and audio profile + agoraEngine?.setAudioProfile(Constants.AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO); + agoraEngine?.setAudioScenario(Constants.AUDIO_SCENARIO_GAME_STREAMING); + return result + } + ``` + - setAudioProfile + - setAudioScenario + + + ```csharp + // Specify the audio scenario and audio profile + agoraEngine.SetAudioProfile(AUDIO_PROFILE_TYPE.AUDIO_PROFILE_DEFAULT, AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_CHATROOM); + ``` + + - SetAudioProfile + + + - SetAudioProfile + + + + ```swift + agoraEngine.setAudioProfile(.musicHighQualityStereo) + agoraEngine.setAudioScenario(.gameStreaming) + ``` + + + - setAudioProfile(_:) + - setAudioScenario(_:) + + + - setAudioProfile(_:) + - setAudioScenario(_:) + + diff --git a/assets/code/video-sdk/audio-voice-effects/set-audio-route.mdx b/assets/code/video-sdk/audio-voice-effects/set-audio-route.mdx new file mode 100644 index 000000000..d5309f7b9 --- /dev/null +++ b/assets/code/video-sdk/audio-voice-effects/set-audio-route.mdx @@ -0,0 +1,87 @@ + + ```kotlin + fun setAudioRoute(enableSpeakerPhone: Boolean) { + // Disable the default audio route + agoraEngine?.setDefaultAudioRoutetoSpeakerphone(false) + // Enable or disable the speakerphone temporarily + agoraEngine?.setEnableSpeakerphone(enableSpeakerPhone) + } + ``` + - setDefaultAudioRoutetoSpeakerphone + - setEnableSpeakerphone + + + ```swift + func setAudioRoute(enableSpeakerPhone: Bool) { + // Disable the default audio route + agoraEngine.setDefaultAudioRouteToSpeakerphone(false) + // Enable or disable the speakerphone temporarily + agoraEngine.setEnableSpeakerphone(enableSpeakerPhone) + } + ``` + + + - setDefaultAudioRouteToSpeakerphone(_:) + - setEnableSpeakerphone(_:) + + + - setDefaultAudioRouteToSpeakerphone(_:) + - setEnableSpeakerphone(_:) + + + + ```csharp + #if (UNITY_ANDROID || UNITY_IOS) + agoraEngine.SetDefaultAudioRouteToSpeakerphone(!enableSpeakerPhone); // Disables the default audio route. + agoraEngine.SetEnableSpeakerphone(enableSpeakerPhone); // Enables or disables the speakerphone temporarily. + #endif + ``` + The `SetDefaultAudioRouteToSpeakerphone` and `SetEnableSpeakerphone` methods applies to Android and iOS only. + + - SetDefaultAudioRouteToSpeakerphone + - SetEnableSpeakerphone + + + - SetDefaultAudioRouteToSpeakerphone + - SetEnableSpeakerphone + + + + ```typescript + // Fetch the available audio playback devices when the component mounts + useEffect(() => { + navigator.mediaDevices?.enumerateDevices?.().then((devices) => { + try { + const playbackDevices = devices.filter((device) => device.kind === "audiooutput"); + setPlaybackDevices(playbackDevices); + setShowDropdown(playbackDevices.length > 0); + } catch (error) { + console.error("Error enumerating playback devices:", error); + } + }) + .catch((error) => { + console.error(error); + }); + }, []); + + // Event handler for changing the audio playback device + const handleAudioRouteChange = () => { + if (audioFileTrack) { + const deviceID = playoutDeviceRef.current?.value; + if (deviceID) { + console.log("The selected device id is: " + deviceID); + try { + audioFileTrack.setPlaybackDevice(deviceID) + .then(() => {console.log("Audio route changed")}) + .catch((error) => {console.error(error);}); + } catch (error) { + console.error("Error setting playback device:", error); + } + } + } + }; + ``` + + - setPlaybackDevice + + diff --git a/assets/code/video-sdk/audio-voice-effects/set-variables.mdx b/assets/code/video-sdk/audio-voice-effects/set-variables.mdx new file mode 100644 index 000000000..2d186262b --- /dev/null +++ b/assets/code/video-sdk/audio-voice-effects/set-variables.mdx @@ -0,0 +1,29 @@ + + ```kotlin + private var audioEffectManager: IAudioEffectManager? = null + ``` + + + ```csharp + // Internal fields for managing audio and voice effects + internal int soundEffectId = 1; // Unique identifier for the sound effect file + internal AUDIO_MIXING_STATE_TYPE audioMixingState; + internal bool isEffectFinished = false; + internal bool enableSpeakerPhone = false; + ``` + + + ```typescript + const [isAudioMixing, setAudioMixing] = useState(false); + const [audioFileTrack, setAudioFileTrack] = useState(null); + const [showDropdown, setShowDropdown] = useState(false); + const [playbackDevices, setPlaybackDevices] = useState([]); + const playoutDeviceRef = useRef(null); + const connectionState = useConnectionState(); + ``` + + + ```swift + var audioEffectId: Int32 = .random(in: 1000...10_000) + ``` + diff --git a/assets/code/video-sdk/audio-voice-effects/stop-start-mixing.mdx b/assets/code/video-sdk/audio-voice-effects/stop-start-mixing.mdx new file mode 100644 index 000000000..4b2027af0 --- /dev/null +++ b/assets/code/video-sdk/audio-voice-effects/stop-start-mixing.mdx @@ -0,0 +1,145 @@ + + ```kotlin + fun startMixing(audioFilePath: String, loopBack: Boolean, cycle: Int, startPos: Int) { + agoraEngine?.startAudioMixing(audioFilePath, loopBack, cycle, startPos) + } + + fun stopMixing() { + agoraEngine?.stopAudioMixing() + } + ``` + - startAudioMixing + - stopAudioMixing + + + ```swift + func startMixing( + audioFilePath: String, loopBack: Bool, + cycle: Int, startPos: Int + ) { + agoraEngine.startAudioMixing( + audioFilePath, loopback: loopBack, + cycle: cycle, startPos: startPos + ) + } + + func stopMixing() { + agoraEngine.stopAudioMixing() + } + ``` + + + - startAudioMixing(_:loopback:cycle:startPos:) + - stopAudioMixing() + + + - startAudioMixing(_:loopback:cycle:startPos:) + - stopAudioMixing() + + + + + ```csharp + + // Method to start audio mixing + public void StartAudioMixing() + { + agoraEngine.StartAudioMixing(configData.audioFileURL, false, 1); + } + + // Method to pause audio mixing. + public void PauseAudioMixing() + { + agoraEngine.PauseAudioMixing(); + } + + // Method to resume audio mixing + public void ResumeAudioMixing() + { + agoraEngine.ResumeAudioMixing(); + } + + // Method to stop audio mixing + public void StopAudioMixing() + { + agoraEngine.StopAudioMixing(); + } + + // Return the audio mixing state + public AUDIO_MIXING_STATE_TYPE GetAudioMixingState() + { + return audioMixingState; + } + ``` + + - StartAudioMixing + - PauseAudioMixing + - ResumeAudioMixing + - StopAudioMixing + + + - StartAudioMixing + - PauseAudioMixing + - ResumeAudioMixing + - StopAudioMixing + + + + ```typescript + // Event handler for selecting an audio file + const handleFileChange = (event: React.ChangeEvent) => { + if (event.target.files && event.target.files.length > 0) { + const selectedFile = event.target.files[0]; + try + { + AgoraRTC.createBufferSourceAudioTrack({ source: selectedFile }) + .then((track) => {setAudioFileTrack(track)}) + .catch((error) => {console.error(error);}) + } catch (error) { + console.error("Error creating buffer source audio track:", error); + } + } + }; + + const AudioMixing: React.FC<{ track: IBufferSourceAudioTrack }> = ({ track }) => { + usePublish([track]); + const agoraEngine = useRTCClient(); + + useEffect(() => { + track.startProcessAudioBuffer(); + track.play(); // to play the track for the local user + agoraEngine.publish(track) + .then(() => { + console.log("Audio mixing track published"); + }) + .catch((error) => { + console.log(console.log(error)); + }); + return () => { + track.stopProcessAudioBuffer(); + track.stop(); + agoraEngine.unpublish(track) + .then(() => { + console.log("Audio mixing track unpublished"); + }) + .catch((error) => { + console.log(console.log(error)); + }); + }; + }, [track]); + return
Audio mixing is in progress
; + }; + ``` + - createBufferSourceAudioTrack + + - startProcessAudioBuffer + + - stopProcessAudioBuffer + + - stop + + - publish + + - unpublish + +
\ No newline at end of file diff --git a/assets/code/video-sdk/audio-voice-effects/swift/apply-voice-effects.mdx b/assets/code/video-sdk/audio-voice-effects/swift/apply-voice-effects.mdx deleted file mode 100644 index 1936c62d0..000000000 --- a/assets/code/video-sdk/audio-voice-effects/swift/apply-voice-effects.mdx +++ /dev/null @@ -1,86 +0,0 @@ - -```swift -@objc func applyVoiceEffectBtnClicked() { - voiceEffectIndex += 1 - // Turn off any previous effects - agoraEngine.setVoiceBeautifierPreset(AgoraVoiceBeautifierPreset.presetOff) - agoraEngine.setAudioEffectPreset(AgoraAudioEffectPreset.off) - agoraEngine.setVoiceConversionPreset(AgoraVoiceConversionPreset.off) - agoraEngine.setLocalVoiceFormant(0.0) - - if (voiceEffectIndex == 1) { - agoraEngine.setVoiceBeautifierPreset(AgoraVoiceBeautifierPreset.presetChatBeautifierMagnetic) - applyVoiceEffectBtn.title = "Chat Beautifier" - } else if (voiceEffectIndex == 2) { - agoraEngine.setVoiceBeautifierPreset(AgoraVoiceBeautifierPreset.presetSingingBeautifier) - applyVoiceEffectBtn.title = "Singing Beautifier" - } else if (voiceEffectIndex == 3) { - // Modify the timbre using the formantRatio - // Range is [-1.0, 1.0], [giant, child] default value is 0. - agoraEngine.setLocalVoiceFormant(0.6) - applyVoiceEffectBtn.title = "Voice effect: Adjust Formant" - } else if (voiceEffectIndex == 4) { - agoraEngine.setAudioEffectPreset(AgoraAudioEffectPreset.voiceChangerEffectHulk) - applyVoiceEffectBtn.title = "Hulk" - } else if (voiceEffectIndex == 5) { - agoraEngine.setVoiceConversionPreset(AgoraVoiceConversionPreset.changerBass) - applyVoiceEffectBtn.title = "Voice Changer" - } else if (voiceEffectIndex == 6) { - // Sets the local voice equalization. - // The first parameter sets the band frequency. The value ranges between 0 and 9. - // Each value represents the center frequency of the band: - // 31, 62, 125, 250, 500, 1k, 2k, 4k, 8k, and 16k Hz. - // The second parameter sets the gain of each band between -15 and 15 dB. - // The default value is 0. - agoraEngine.setLocalVoiceEqualizationOf(AgoraAudioEqualizationBandFrequency.band500, withGain: 3) - agoraEngine.setLocalVoicePitch(0.5) - applyVoiceEffectBtn.title = "Voice Equalization" - } else if (voiceEffectIndex > 6) { // Remove all effects - voiceEffectIndex = 0 - agoraEngine.setLocalVoicePitch(1.0) - agoraEngine.setLocalVoiceEqualizationOf(AgoraAudioEqualizationBandFrequency.band500, withGain: 0) - applyVoiceEffectBtn.title = "Voice effect" - } -} -``` - - -```swift -@objc func applyVoiceEffectBtnClicked() { - voiceEffectIndex += 1 - // Turn off any previous effects - agoraEngine.setVoiceBeautifierPreset(AgoraVoiceBeautifierPreset.presetOff) - agoraEngine.setAudioEffectPreset(AgoraAudioEffectPreset.off) - agoraEngine.setVoiceConversionPreset(AgoraVoiceConversionPreset.off) - - if (voiceEffectIndex == 1) { - agoraEngine.setVoiceBeautifierPreset(AgoraVoiceBeautifierPreset.presetChatBeautifierMagnetic) - applyVoiceEffectBtn.setTitle("Voice effect: Chat Beautifier", for: .normal) - } else if (voiceEffectIndex == 2) { - agoraEngine.setVoiceBeautifierPreset(AgoraVoiceBeautifierPreset.presetSingingBeautifier) - applyVoiceEffectBtn.setTitle("Voice effect: Singing Beautifier", for: .normal) - } else if (voiceEffectIndex == 3) { - agoraEngine.setAudioEffectPreset(AgoraAudioEffectPreset.voiceChangerEffectHulk) - applyVoiceEffectBtn.setTitle("Audio effect: Hulk", for: .normal) - } else if (voiceEffectIndex == 4) { - agoraEngine.setVoiceConversionPreset(AgoraVoiceConversionPreset.changerBass) - applyVoiceEffectBtn.setTitle("Audio effect: Voice Changer", for: .normal) - } else if (voiceEffectIndex == 5) { - // Sets the local voice equalization. - // The first parameter sets the band frequency. The value ranges between 0 and 9. - // Each value represents the center frequency of the band: - // 31, 62, 125, 250, 500, 1k, 2k, 4k, 8k, and 16k Hz. - // The second parameter sets the gain of each band between -15 and 15 dB. - // The default value is 0. - agoraEngine.setLocalVoiceEqualizationOf(AgoraAudioEqualizationBandFrequency.band500, withGain: 3) - agoraEngine.setLocalVoicePitch(0.5) - applyVoiceEffectBtn.setTitle("Audio effect: Voice Equalization", for: .normal) - } else if (voiceEffectIndex > 5) { // Remove all effects - voiceEffectIndex = 0 - agoraEngine.setLocalVoicePitch(1.0) - agoraEngine.setLocalVoiceEqualizationOf(AgoraAudioEqualizationBandFrequency.band500, withGain: 0) - applyVoiceEffectBtn.setTitle("Apply voice effect", for: .normal) - } -} -``` - \ No newline at end of file diff --git a/assets/code/video-sdk/audio-voice-effects/swift/create-ui.mdx b/assets/code/video-sdk/audio-voice-effects/swift/create-ui.mdx deleted file mode 100644 index 889d022c0..000000000 --- a/assets/code/video-sdk/audio-voice-effects/swift/create-ui.mdx +++ /dev/null @@ -1,15 +0,0 @@ - -```swift -var audioMixingBtn: NSButton! -var playAudioEffectBtn: NSButton! -var applyVoiceEffectBtn: NSButton! -``` - - -```swift -var audioMixingBtn: UIButton! -var playAudioEffectBtn: UIButton! -var applyVoiceEffectBtn: UIButton! -var speakerphoneSwitch: UISwitch! -``` - \ No newline at end of file diff --git a/assets/code/video-sdk/audio-voice-effects/swift/pause-play-resume.mdx b/assets/code/video-sdk/audio-voice-effects/swift/pause-play-resume.mdx deleted file mode 100644 index 48bff3e88..000000000 --- a/assets/code/video-sdk/audio-voice-effects/swift/pause-play-resume.mdx +++ /dev/null @@ -1,56 +0,0 @@ - -```swift -@objc func playAudioEffectBtnClicked() { - if (soundEffectStatus == 0) { // Stopped - agoraEngine.playEffect( - soundEffectId, // The ID of the sound effect file. - filePath: soundEffectFilePath, // The path of the sound effect file. - loopCount: 0, // The number of sound effect loops. -1 means an infinite loop. 0 means once. - pitch: 1, // The pitch of the audio effect. 1 represents the original pitch. - pan: 0.0, // The spatial position of the audio effect. 0.0 represents that the audio effect plays in the front. - gain: 100, // The volume of the audio effect. 100 represents the original volume. - publish: true,// Whether to publish the audio effect to remote users. - startPos: 0 // The playback starting position of the audio effect file in ms. - ); - playAudioEffectBtn.title = "Pause" - soundEffectStatus = 1 - } else if (soundEffectStatus == 1) { // Playing - agoraEngine.pauseEffect(soundEffectId) - soundEffectStatus = 2 - playAudioEffectBtn.title = "Resume" - } else if (soundEffectStatus == 2) { // Paused - agoraEngine.resumeEffect(soundEffectId) - soundEffectStatus = 1 - playAudioEffectBtn.title = "Pause" - } -} -``` - - -```swift -@objc func playAudioEffectBtnClicked() { - if (soundEffectStatus == 0) { // Stopped - agoraEngine.playEffect( - soundEffectId, // The ID of the sound effect file. - filePath: soundEffectFilePath, // The path of the sound effect file. - loopCount: 0, // The number of sound effect loops. -1 means an infinite loop. 0 means once. - pitch: 1, // The pitch of the audio effect. 1 represents the original pitch. - pan: 0.0, // The spatial position of the audio effect. 0.0 represents that the audio effect plays in the front. - gain: 100, // The volume of the audio effect. 100 represents the original volume. - publish: true,// Whether to publish the audio effect to remote users. - startPos: 0 // The playback starting position of the audio effect file in ms. - ); - playAudioEffectBtn.setTitle("Pause sound effect", for: .normal) - soundEffectStatus = 1 - } else if (soundEffectStatus == 1) { // Playing - agoraEngine.pauseEffect(soundEffectId) - soundEffectStatus = 2 - playAudioEffectBtn.setTitle("Resume sound effect", for: .normal) - } else if (soundEffectStatus == 2) { // Paused - agoraEngine.resumeEffect(soundEffectId) - soundEffectStatus = 1 - playAudioEffectBtn.setTitle("Pause sound effect", for: .normal) - } -} -``` - \ No newline at end of file diff --git a/assets/code/video-sdk/audio-voice-effects/swift/set-audio-route.mdx b/assets/code/video-sdk/audio-voice-effects/swift/set-audio-route.mdx deleted file mode 100644 index a2477e020..000000000 --- a/assets/code/video-sdk/audio-voice-effects/swift/set-audio-route.mdx +++ /dev/null @@ -1,18 +0,0 @@ - - ```swift - @objc func speakerphoneSwitchValueChanged(sender: NSSwitch) { - agoraEngine.setDefaultAudioRouteToSpeakerphone(false) // Disables the default audio route. - print("Changing speakerphone enabled state to \(sender.isEnabled)") - agoraEngine.setEnableSpeakerphone(sender.isOn) // Enables or disables the speakerphone temporarily. - } - ``` - - - ```swift - @objc func speakerphoneSwitchValueChanged(sender: UISwitch) { - agoraEngine.setDefaultAudioRouteToSpeakerphone(false) // Disables the default audio route. - print("Changing speakerphone enabled state to \(sender.isOn)") - agoraEngine.setEnableSpeakerphone(sender.isOn) // Enables or disables the speakerphone temporarily. -} - ``` - \ No newline at end of file diff --git a/assets/code/video-sdk/audio-voice-effects/swift/stop-start-mixing.mdx b/assets/code/video-sdk/audio-voice-effects/swift/stop-start-mixing.mdx deleted file mode 100644 index 5150a6eff..000000000 --- a/assets/code/video-sdk/audio-voice-effects/swift/stop-start-mixing.mdx +++ /dev/null @@ -1,40 +0,0 @@ - -```swift -@objc func audioMixingBtnClicked() { - audioPlaying = !audioPlaying - - if (audioPlaying) { - audioMixingBtn.title = "Stop" - let result = agoraEngine.startAudioMixing(audioFilePath, loopback: false, cycle: -1, startPos: 0) - if (result == 0) { - showMessage(title: "Audio Mixing", text: "Audio playing") - } else { - showMessage(title: "Audio Mixing", text: "Failed to play audio") - } - } else { - agoraEngine.stopAudioMixing() - audioMixingBtn.title = "Play" - } -} -``` - - -```swift -@objc func audioMixingBtnClicked() { - audioPlaying = !audioPlaying - - if (audioPlaying) { - audioMixingBtn.setTitle("Stop playing audio", for: .normal) - let result = agoraEngine.startAudioMixing(audioFilePath, loopback: false, replace: false, cycle: -1, startPos: 0) - if (result == 0) { - showMessage(title: "Audio Mixing", text: "Audio playing") - } else { - showMessage(title: "Audio Mixing", text: "Failed to play audio") - } - } else { - agoraEngine.stopAudioMixing() - audioMixingBtn.setTitle("Play Audio", for: .normal) - } -} -``` - \ No newline at end of file diff --git a/assets/code/video-sdk/audio-voice-effects/swift/update-ui.mdx b/assets/code/video-sdk/audio-voice-effects/update-ui.mdx similarity index 100% rename from assets/code/video-sdk/audio-voice-effects/swift/update-ui.mdx rename to assets/code/video-sdk/audio-voice-effects/update-ui.mdx diff --git a/assets/code/video-sdk/authentication-workflow/add-variables.mdx b/assets/code/video-sdk/authentication-workflow/add-variables.mdx new file mode 100644 index 000000000..974f6f29c --- /dev/null +++ b/assets/code/video-sdk/authentication-workflow/add-variables.mdx @@ -0,0 +1,44 @@ + + + ```swift + // Update with the temporary token generated in Agora Console. + var token = "" + + // Update with the channel name you used to generate the token in Agora Console. + var channelName = "" + + // Store the name of the channel to join + var channelTextField: NSTextField! + + // Add the base URL to your token server. For example, https://agora-token-service-production-92ff.up.railway.app" + var serverUrl = "" + // The ID of the app user + var userID = 0 + // Expire time in Seconds. + var tokenExpireTime = 40 + ``` + + + ```swift + // Update with the temporary token generated in Agora Console. + var token = "" + + // Update with the channel name you used to generate the token in Agora Console. + var channelName = "" + + // Store the name of the channel to join + var channelTextField: UITextField! + + // Add the base URL to your token server. For example, https://agora-token-service-production-92ff.up.railway.app" + var serverUrl = "" + // The ID of the app user + var userID = 0 + // Expire time in Seconds. + var tokenExpireTime = 40 + ``` + + + ```javascript + let role = "publisher"; // set the role to "publisher" or "subscriber" as appropriate + ``` + \ No newline at end of file diff --git a/assets/code/video-sdk/authentication-workflow/event-handler.mdx b/assets/code/video-sdk/authentication-workflow/event-handler.mdx new file mode 100644 index 000000000..f5f76cd38 --- /dev/null +++ b/assets/code/video-sdk/authentication-workflow/event-handler.mdx @@ -0,0 +1,173 @@ + + ```kotlin + // Listen for the event that a token is about to expire + override val iRtcEngineEventHandler: IRtcEngineEventHandler + get() = object : IRtcEngineEventHandler() { + // Listen for the event that the token is about to expire + override fun onTokenPrivilegeWillExpire(token: String) { + sendMessage("Token is about to expire") + // Get a new token + fetchToken(channelName, object : TokenCallback { + override fun onTokenReceived(rtcToken: String?) { + // Use the token to renew + agoraEngine!!.renewToken(rtcToken) + sendMessage("Token renewed") + } + + override fun onError(errorMessage: String) { + // Handle the error + sendMessage("Error: $errorMessage") + } + }) + super.onTokenPrivilegeWillExpire(token) + } + } + ``` + + - renewToken + - onTokenPrivilegeWillExpire + + + + ```swift + func rtcEngine( + _ engine: AgoraRtcEngineKit, tokenPrivilegeWillExpire token: String + ) { + Task { + if let token = try? await fetchToken( + from: DocsAppConfig.shared.tokenUrl, + channel: DocsAppConfig.shared.channel, + role: .broadcaster + ) { self.agoraEngine.renewToken(token) } + } + } + ``` + + + - renewToken(_:) + - rtcEngine(\_:tokenPrivilegeWillExpire:)(\_:) + + + - renewToken(_:) + - rtcEngine(\_:tokenPrivilegeWillExpire:)(\_:) + + + + + ```csharp + internal class AuthenticationWorkflowEventHandler : UserEventHandler + { + private AuthenticationWorkflowManager authenticationWorkflowManager; + + internal AuthenticationWorkflowEventHandler(AuthenticationWorkflowManager refAuthenticationWorkflow) : base(refAuthenticationWorkflow) + { + authenticationWorkflowManager = + refAuthenticationWorkflow; + } + + public override async void OnTokenPrivilegeWillExpire(RtcConnection connection, string token) + { + Debug.Log("Token Expired"); + // Retrieve a fresh token from the token server. + await authenticationWorkflowManager.FetchToken(); + authenticationWorkflowManager.RenewToken(); + } + + public override async void OnClientRoleChanged(RtcConnection connection, CLIENT_ROLE_TYPE oldRole, CLIENT_ROLE_TYPE newRole, ClientRoleOptions newRoleOptions) + { + // Retrieve a fresh token from the token server for the new role. + Debug.Log("Role is set to " + newRole.ToString()); + await authenticationWorkflowManager.FetchToken(); + authenticationWorkflowManager.RenewToken(); + } + } + ``` + + - OnTokenPrivilegeWillExpire + - OnClientRoleChanged + + + + ```js + // The following code is solely related to UI implementation and not Agora-specific code + window.onload = async () => { + // Set the project selector + setupProjectSelector(); + + const handleVSDKEvents = (eventName, ...args) => { + switch (eventName) { + case "user-published": + if (args[1] == "video") { + // Retrieve the remote video track. + channelParameters.remoteVideoTrack = args[0].videoTrack; + // Retrieve the remote audio track. + channelParameters.remoteAudioTrack = args[0].audioTrack; + // Save the remote user id for reuse. + channelParameters.remoteUid = args[0].uid.toString(); + // Specify the ID of the DIV container. You can use the uid of the remote user. + remotePlayerContainer.id = args[0].uid.toString(); + channelParameters.remoteUid = args[0].uid.toString(); + remotePlayerContainer.textContent = + "Remote user " + args[0].uid.toString(); + // Append the remote container to the page body. + document.body.append(remotePlayerContainer); + // Play the remote video track. + channelParameters.remoteVideoTrack.play(remotePlayerContainer); + } + // Subscribe and play the remote audio track If the remote user publishes the audio track only. + if (args[1] == "audio") { + // Get the RemoteAudioTrack object in the AgoraRTCRemoteUser object. + channelParameters.remoteAudioTrack = args[0].audioTrack; + // Play the remote audio track. No need to pass any DOM element. + channelParameters.remoteAudioTrack.play(); + } + } + }; + ``` + + + ```js + // The following code is solely related to UI implementation and not Agora-specific code + window.onload = async () => { + // Set the project selector + setupProjectSelector(); + + const handleVSDKEvents = (eventName, ...args) => { + switch (eventName) { + case "user-published": + // Subscribe and play the remote audio track If the remote user publishes the audio track only. + if (args[1] == "audio") { + // Get the RemoteAudioTrack object in the AgoraRTCRemoteUser object. + channelParameters.remoteAudioTrack = args[0].audioTrack; + // Play the remote audio track. No need to pass any DOM element. + channelParameters.remoteAudioTrack.play(); + } + } + }; + ``` + + + + ```typescript + const useTokenWillExpire = () => { + const agoraEngine = useRTCClient(); + useClientEvent(agoraEngine, "token-privilege-will-expire", () => { + if (config.serverUrl !== "") { + fetchRTCToken(config.channelName) + .then((token) => { + console.log("RTC token fetched from server: ", token); + if (token) return agoraEngine.renewToken(token); + }) + .catch((error) => { + console.error(error); + }); + } else { + console.log("Please make sure you specified the token server URL in the configuration file"); + } + }); + }; + ``` + - useRTCClient + - useClientEvent + - renewToken + diff --git a/assets/code/video-sdk/authentication-workflow/fetch-token.mdx b/assets/code/video-sdk/authentication-workflow/fetch-token.mdx new file mode 100644 index 000000000..fa3ebf199 --- /dev/null +++ b/assets/code/video-sdk/authentication-workflow/fetch-token.mdx @@ -0,0 +1,182 @@ + + ```kotlin + fun fetchToken(channelName: String, uid: Int, callback: TokenCallback) { + val tokenRole = if (isBroadcaster) 1 else 2 + // Prepare the Url + val urlLString = "$serverUrl/rtc/$channelName/$tokenRole/uid/$uid/?expiry=$tokenExpiryTime" + + val client = OkHttpClient() + + // Create a request + val request: Request = Builder() + .url(urlLString) + .header("Content-Type", "application/json; charset=UTF-8") + .get() + .build() + + // Send the async http request + val call = client.newCall(request) + call.enqueue(object : Callback { + // Receive the response in a callback + @Throws(IOException::class) + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + try { + // Extract rtcToken from the response + val responseBody = response.body!!.string() + val jsonObject = JSONObject(responseBody) + val rtcToken = jsonObject.getString("rtcToken") + // Return the token + callback.onTokenReceived(rtcToken) + } catch (e: JSONException) { + e.printStackTrace() + callback.onError("Invalid token response") + } + } else { + callback.onError("Token request failed") + } + } + + override fun onFailure(call: Call, e: IOException) { + callback.onError("IOException: $e") + } + }) + } + ``` + + + + ```swift + func fetchToken( + from tokenUrl: String, channel: String, + role: AgoraClientRole, userId: UInt = 0 + ) async throws -> String { + guard let url = URL(string: "\(tokenUrl)/getToken") else { + throw URLError(.badURL) + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + + var userData = [ + "tokenType": "rtc", + "uid": String(userId), + "role": role == .broadcaster ? "publisher" : "subscriber", + "channel": channel + ] + + let requestData = try JSONEncoder().encode(userData) + request.httpBody = requestData + + let (data, _) = try await URLSession.shared.data(for: request) + let tokenResponse = try JSONDecoder().decode(TokenResponse.self, from: data) + + return tokenResponse.token + } + + /// A Codable struct representing the token server response. + struct TokenResponse: Codable { + /// Value of the RTC Token. + public let token: String + } + ``` + + + + ```csharp + public async Task FetchToken() + { + if(userRole == "Host") + { + role = 1; + } + else if (userRole == "Audience") + { + role = 2; + } + + string url = string.Format("{0}/rtc/{1}/{2}/uid/{3}/?expiry={4}", configData.tokenUrl, configData.channelName, role ,configData.uid, configData.tokenExpiryTime); + + UnityWebRequest request = UnityWebRequest.Get(url); + + var operation = request.SendWebRequest(); + + while (!operation.isDone) + { + await Task.Yield(); + } + + if (request.isNetworkError || request.isHttpError) + { + Debug.Log(request.error); + return; + } + + TokenStruct tokenInfo = JsonUtility.FromJson(request.downloadHandler.text); + Debug.Log("Retrieved token : " + tokenInfo.rtcToken); + _token = tokenInfo.rtcToken; + _channelName = configData.channelName; + } + ``` + + +```javascript + // Get the config + const config = agoraManager.config; + + // Fetches the RTC token for stream channels + async function fetchToken(uid, channelName) { + if (config.serverUrl !== "") { + try { + const res = await fetch( + config.proxyUrl + + config.serverUrl + + "/rtc/" + + channelName + + "/" + + role + + "/uid/" + + uid + + "/?expiry=" + + config.tokenExpiryTime, + { + headers: { + "X-Requested-With": "XMLHttpRequest", + }, + } + ); + const data = await res.text(); + const json = await JSON.parse(data); + console.log("Video SDK token fetched from server: ", json.rtcToken); + return json.rtcToken; + } catch (err) { + console.log(err); + } + } else { + return config.token; + } + } +``` + + + ```typescript + async function fetchRTCToken(channelName: string) { + if (config.serverUrl !== "") { + try { + const response = await fetch( + `${config.proxyUrl}${config.serverUrl}/rtc/${channelName}/publisher/uid/${config.uid}/?expiry=${config.tokenExpiryTime}` + ); + const data = await response.json() as { rtcToken: string }; + console.log("RTC token fetched from server: ", data.rtcToken); + return data.rtcToken; + } catch (error) { + console.error(error); + throw error; + } + } else { + return config.rtcToken; + } + } + ``` + diff --git a/assets/code/video-sdk/authentication-workflow/import-library.mdx b/assets/code/video-sdk/authentication-workflow/import-library.mdx new file mode 100644 index 000000000..6b8340b2d --- /dev/null +++ b/assets/code/video-sdk/authentication-workflow/import-library.mdx @@ -0,0 +1,38 @@ + + ```kotlin + import io.agora.rtc2.* + import io.agora.agora_manager.AgoraManager + import okhttp3.* + import okhttp3.Request.* + import android.content.Context + import java.io.IOException + import java.net.MalformedURLException + import java.net.URL + ``` + + + ```swift + import SwiftUI + import AgoraRtcKit + ``` + + + ```csharp + using UnityEngine.Networking; + using Agora.Rtc; + using System.Threading.Tasks; + ``` + + + ```javascript + import AgoraManager from "../agora_manager/agora_manager.js"; + import AgoraRTC from "agora-rtc-sdk-ng"; + ``` + + + ```typescript + import { AgoraManager } from "../agora-manager/agoraManager"; + import config from "../agora-manager/config"; + import { useClientEvent, useRTCClient } from "agora-rtc-react"; + ``` + diff --git a/assets/code/video-sdk/authentication-workflow/join-channel.mdx b/assets/code/video-sdk/authentication-workflow/join-channel.mdx new file mode 100644 index 000000000..4ed4cab46 --- /dev/null +++ b/assets/code/video-sdk/authentication-workflow/join-channel.mdx @@ -0,0 +1,220 @@ + + + ```kotlin + open fun joinChannelWithToken(channelName: String): Int { + if (agoraEngine == null) setupAgoraEngine() + return if (isValidURL(serverUrl)) { // A valid server url is available + // Fetch a token from the server for channelName + fetchToken(channelName, object : TokenCallback { + override fun onTokenReceived(rtcToken: String?) { + // Handle the received rtcToken + joinChannel(channelName, rtcToken) + } + + override fun onError(errorMessage: String) { + // Handle the error + sendMessage("Error: $errorMessage") + } + }) + 0 + } else { // use the token from the config.json file + val token = config!!.optString("rtcToken") + joinChannel(channelName, token) + } + } + ``` + + - joinChannel + + + + + ```swift + // This method is specifically used by the sample app. If there is a tokenURL, it will attempt to retrieve a token from there. + internal func joinChannel(_ channel: String, uid: UInt? = nil) async -> Int32 { + let userId = uid ?? DocsAppConfig.shared.uid + var token = DocsAppConfig.shared.rtcToken + if !DocsAppConfig.shared.tokenUrl.isEmpty { + do { + token = try await self.fetchToken( + from: DocsAppConfig.shared.tokenUrl, channel: channel, + role: self.role, userId: userId + ) + } catch { + print("token server fetch failed: \(error.localizedDescription)") + } + } + return self.joinChannel(channel, token: token, uid: userId, info: nil) + } + + // Joins a channel, starting the connection to a session. + open func joinChannel( + _ channel: String, token: String? = nil, uid: UInt = 0, info: String? = nil + ) async -> Int32 { + if await !AgoraManager.checkForPermissions() { + DispatchQueue.main.async { + self.label = """ + Camera and microphone permissions were not granted. + Check your security settings and try again. + """ + } + return -3 + } + + return self.agoraEngine.joinChannel( + byToken: token, channelId: channel, + info: info, uid: uid + ) + } + ``` + - joinChannel(byToken:channelId:info:uid:joinSuccess:) + + + + + ```swift + // This method is specifically used by the sample app. If there is a tokenURL, it will attempt to retrieve a token from there. + open func joinChannel(_ channel: String) async -> Int32 { + if let rtcToken = DocsAppConfig.shared.rtcToken, !rtcToken.isEmpty { + return self.joinChannel( + channel, token: DocsAppConfig.shared.rtcToken, + uid: DocsAppConfig.shared.uid, info: nil + ) + } + var token: String? + if !DocsAppConfig.shared.tokenUrl.isEmpty { + do { + token = try await self.fetchToken( + from: DocsAppConfig.shared.tokenUrl, channel: channel, role: self.role + ) + } catch { + print("token server fetch failed: \(error.localizedDescription)") + } + } + return self.joinChannel(channel, token: token, uid: DocsAppConfig.shared.uid, info: nil) + } + // Joins a channel, starting the connection to a session. + open func joinChannel( + _ channel: String, token: String? = nil, uid: UInt = 0, info: String? = nil + ) -> Int32 { + self.agoraEngine.joinChannel(byToken: token, channelId: channel, info: info, uid: uid) + } + ``` + - joinChannel(byToken:channelId:info:uid:joinSuccess:) + + + + + ```csharp + public override async void Join() + { + if (configData.tokenUrl == "") + { + Debug.Log("Specify a valid token server URL inside `config.json` if you wish to fetch token from the server"); + } + else + { + await FetchToken(); + } + + // Join the channel. + base.Join(); + } + ``` + + + + +```javascript +const joinWithToken = async (localPlayerContainer, channelParameters) => { + const token = await fetchToken(config.uid, config.channelName); + await agoraManager + .getAgoraEngine() + .join(config.appId, config.channelName, token, config.uid); + // Create a local audio track from the audio sampled by a microphone. + channelParameters.localAudioTrack = + await AgoraRTC.createMicrophoneAudioTrack(); + // Create a local video track from the video captured by a camera. + channelParameters.localVideoTrack = await AgoraRTC.createCameraVideoTrack(); + // Append the local video container to the page body. + document.body.append(localPlayerContainer); + // Publish the local audio and video tracks in the channel. + await agoraManager + .getAgoraEngine() + .publish([ + channelParameters.localAudioTrack, + channelParameters.localVideoTrack, + ]); + // Play the local video track. + channelParameters.localVideoTrack.play(localPlayerContainer); + }; +``` + + +```javascript +const joinWithToken = async (localPlayerContainer, channelParameters) => { + const token = await fetchToken(config.uid, config.channelName); + await agoraManager + .getAgoraEngine() + .join(config.appId, config.channelName, token, config.uid); + // Create a local audio track from the audio sampled by a microphone. + channelParameters.localAudioTrack = + await AgoraRTC.createMicrophoneAudioTrack(); + // Publish the local audio and video tracks in the channel. + await agoraManager + .getAgoraEngine() + .publish([ + channelParameters.localAudioTrack, + ]); + }; +``` + + + + ```typescript + function AuthenticationWorkflowManager(props: { children?: React.ReactNode }) { + const [channelName, setChannelName] = useState(""); + const [joined, setJoined] = useState(false); + + useTokenWillExpire(); + + const fetchTokenFunction = async () => { + if (config.serverUrl !== "" && channelName !== "") { + try { + const token = await fetchRTCToken(channelName) as string; + config.rtcToken = token; + config.channelName = channelName; + setJoined(true) + } catch (error) { + console.error(error); + } + } else { + console.log("Please make sure you specified the token server URL in the configuration file"); + } + }; + + return ( +
+ {!joined ? ( + <> + setChannelName(e.target.value)} + placeholder="Channel name" /> + + {props.children} + + ) : ( + <> + + + {props.children} + + + )} +
+ ); + } + ``` +
diff --git a/assets/code/video-sdk/authentication-workflow/renew-token.mdx b/assets/code/video-sdk/authentication-workflow/renew-token.mdx new file mode 100644 index 000000000..1d516345b --- /dev/null +++ b/assets/code/video-sdk/authentication-workflow/renew-token.mdx @@ -0,0 +1,48 @@ + + ```kotlin + agoraEngine!!.renewToken(rtcToken) + ``` + + + + ```swift + self.agoraEngine.renewToken(token) + ``` + + + - renewToken(_:) + + + - renewToken(_:) + + + + + ```csharp + public void RenewToken() + { + if(_token == "") + { + Debug.Log("Token was not retrieved"); + return; + } + + // Update RTC Engine with new token + agoraEngine.RenewToken(_token); + } + ``` + - RenewToken + + + ```javascript + // Renew tokens + agoraManager + .getAgoraEngine() + .on("token-privilege-will-expire", async function () { + options.token = await fetchToken(config.uid, config.channelName); + await agoraManager.getAgoraEngine().renewToken(options.token); + }); + ``` + - renewToken + - token-privilege-will-expire + diff --git a/assets/code/video-sdk/authentication-workflow/swift/specify-channel.mdx b/assets/code/video-sdk/authentication-workflow/specify-channel.mdx similarity index 95% rename from assets/code/video-sdk/authentication-workflow/swift/specify-channel.mdx rename to assets/code/video-sdk/authentication-workflow/specify-channel.mdx index bc84d3da3..ac54a1e40 100644 --- a/assets/code/video-sdk/authentication-workflow/swift/specify-channel.mdx +++ b/assets/code/video-sdk/authentication-workflow/specify-channel.mdx @@ -1,6 +1,6 @@ -``` swift +```swift channelTextField = NSTextField(frame: CGRect(x: 30, y: 30, width: 300, height: 40)) channelTextField.stringValue = "Enter channel name" self.view.addSubview(channelTextField) @@ -10,7 +10,7 @@ self.view.addSubview(channelTextField) -``` swift +```swift channelTextField = UITextField(frame: CGRect(x: 300, y: 20, width: 300, height: 30)) channelTextField.placeholder = "Enter channel name" self.view.addSubview(channelTextField) diff --git a/assets/code/video-sdk/authentication-workflow/swift/add-variables.mdx b/assets/code/video-sdk/authentication-workflow/swift/add-variables.mdx deleted file mode 100644 index ea5705c01..000000000 --- a/assets/code/video-sdk/authentication-workflow/swift/add-variables.mdx +++ /dev/null @@ -1,42 +0,0 @@ - - -``` swift -// Update with the temporary token generated in Agora Console. -var token = "" - -// Update with the channel name you used to generate the token in Agora Console. -var channelName = "" - -// Store the name of the channel to join -var channelTextField: NSTextField! - -// Add the base URL to your token server. For example, https://agora-token-service-production-92ff.up.railway.app" -var serverUrl = "" -// The ID of the app user -var userID = 0 -// Expire time in Seconds. -var tokenExpireTime = 40 -``` - - - - -``` swift -// Update with the temporary token generated in Agora Console. -var token = "" - -// Update with the channel name you used to generate the token in Agora Console. -var channelName = "" - -// Store the name of the channel to join -var channelTextField: UITextField! - -// Add the base URL to your token server. For example, https://agora-token-service-production-92ff.up.railway.app" -var serverUrl = "" -// The ID of the app user -var userID = 0 -// Expire time in Seconds. -var tokenExpireTime = 40 -``` - - \ No newline at end of file diff --git a/assets/code/video-sdk/authentication-workflow/swift/fetch-token.mdx b/assets/code/video-sdk/authentication-workflow/swift/fetch-token.mdx deleted file mode 100644 index e8d7f833f..000000000 --- a/assets/code/video-sdk/authentication-workflow/swift/fetch-token.mdx +++ /dev/null @@ -1,18 +0,0 @@ - - ``` swift - func fetchToken( - from tokenUrl: String, channel: String, - role: AgoraClientRole, userId: UInt = 0 - ) async throws -> String? { - guard !tokenUrl.isEmpty else { return nil } - - guard let tokenServerURL = URL( - string: "\(tokenUrl)/rtc/\(channel)/\(role.rawValue)/uid/\(userId)/" - ) else { return nil } - - let (data, _) = try await URLSession.shared.data(from: tokenServerURL) - let tokenResponse = try JSONDecoder().decode(TokenResponse.self, from: data) - - return tokenResponse.rtcToken - } - ``` diff --git a/assets/code/video-sdk/authentication-workflow/swift/join-channel.mdx b/assets/code/video-sdk/authentication-workflow/swift/join-channel.mdx deleted file mode 100644 index c3dcc97a6..000000000 --- a/assets/code/video-sdk/authentication-workflow/swift/join-channel.mdx +++ /dev/null @@ -1,75 +0,0 @@ - - - -``` swift -// This method is specifically used by the sample app. If there is a tokenURL, it will attempt to retrieve a token from there. -internal func joinChannel(_ channel: String, uid: UInt? = nil) async -> Int32 { - let userId = uid ?? DocsAppConfig.shared.uid - var token = DocsAppConfig.shared.rtcToken - if !DocsAppConfig.shared.tokenUrl.isEmpty { - do { - token = try await self.fetchToken( - from: DocsAppConfig.shared.tokenUrl, channel: channel, - role: self.role, userId: userId - ) - } catch { - print("token server fetch failed: \(error.localizedDescription)") - } - } - return self.joinChannel(channel, token: token, uid: userId, info: nil) -} - -// Joins a channel, starting the connection to a session. -open func joinChannel( - _ channel: String, token: String? = nil, uid: UInt = 0, info: String? = nil -) async -> Int32 { - if await !AgoraManager.checkForPermissions() { - DispatchQueue.main.async { - self.label = """ - Camera and microphone permissions were not granted. - Check your security settings and try again. - """ - } - return -3 - } - - return self.agoraEngine.joinChannel( - byToken: token, channelId: channel, - info: info, uid: uid - ) -} -``` - - - - -``` swift -// This method is specifically used by the sample app. If there is a tokenURL, it will attempt to retrieve a token from there. -open func joinChannel(_ channel: String) async -> Int32 { - if let rtcToken = DocsAppConfig.shared.rtcToken, !rtcToken.isEmpty { - return self.joinChannel( - channel, token: DocsAppConfig.shared.rtcToken, - uid: DocsAppConfig.shared.uid, info: nil - ) - } - var token: String? - if !DocsAppConfig.shared.tokenUrl.isEmpty { - do { - token = try await self.fetchToken( - from: DocsAppConfig.shared.tokenUrl, channel: channel, role: self.role - ) - } catch { - print("token server fetch failed: \(error.localizedDescription)") - } - } - return self.joinChannel(channel, token: token, uid: DocsAppConfig.shared.uid, info: nil) -} -// Joins a channel, starting the connection to a session. -open func joinChannel( - _ channel: String, token: String? = nil, uid: UInt = 0, info: String? = nil -) -> Int32 { - self.agoraEngine.joinChannel(byToken: token, channelId: channel, info: info, uid: uid) -} -``` - - diff --git a/assets/code/video-sdk/cloud-proxy/configure-engine.mdx b/assets/code/video-sdk/cloud-proxy/configure-engine.mdx new file mode 100644 index 000000000..ccf790f89 --- /dev/null +++ b/assets/code/video-sdk/cloud-proxy/configure-engine.mdx @@ -0,0 +1,18 @@ + + ```typescript + export function CloudProxy() { + const agoraEngine = useRTCClient(AgoraRTC.createClient({ codec: "vp8", mode: config.selectedProduct })); + + return ( +
+

Connect through restricted networks with Cloud Proxy

+ + + +
+ ); + } + ``` + - useRTCClient + +
\ No newline at end of file diff --git a/assets/code/video-sdk/cloud-proxy/connection-failed.mdx b/assets/code/video-sdk/cloud-proxy/connection-failed.mdx new file mode 100644 index 000000000..6948c4cf3 --- /dev/null +++ b/assets/code/video-sdk/cloud-proxy/connection-failed.mdx @@ -0,0 +1,63 @@ + Use this event to see when an attempt to connect directly to fails. + + + ```kotlin + override fun onConnectionStateChanged(state: Int, reason: Int) { + if (state == CONNECTION_STATE_FAILED + && reason == CONNECTION_CHANGED_JOIN_FAILED ) { + directConnectionFailed = true + sendMessage("Join failed, reason: $reason") + } else if (state == CONNECTION_CHANGED_SETTING_PROXY_SERVER) { + sendMessage("Proxy server setting changed") + } + } + ``` + - onConnectionStateChanged + + + + ```csharp + // Event handler class to handle the events raised by Agora's RtcEngine instance + internal class CloudProxyEventHandler : UserEventHandler + { + private CloudProxyManager cloudProxy; + internal CloudProxyEventHandler(CloudProxyManager videoSample):base(videoSample) + { + cloudProxy = videoSample; + } + public override void OnConnectionStateChanged(RtcConnection connection, CONNECTION_STATE_TYPE state, CONNECTION_CHANGED_REASON_TYPE reason) + { + if(state == CONNECTION_STATE_TYPE.CONNECTION_STATE_FAILED && reason == CONNECTION_CHANGED_REASON_TYPE.CONNECTION_CHANGED_JOIN_FAILED) + { + cloudProxy.directConnectionFailed = true; + Debug.Log("Join failed, reason: " + reason); + } + else if (reason == CONNECTION_CHANGED_REASON_TYPE.CONNECTION_CHANGED_SETTING_PROXY_SERVER) + { + Debug.Log("Proxy server setting changed"); + } + } + } + ``` + - OnConnectionStateChanged + + + ```swift + func rtcEngine( + _ engine: AgoraRtcEngineKit, + connectionChangedTo state: AgoraConnectionState, + reason: AgoraConnectionChangedReason + ) { + if state == .failed, reason == .reasonJoinFailed { + // connection failed, try connect with proxy + } + } + ``` + + + - rtcEngine(_:connectionChangedTo:reason:) + + + - rtcEngine(_:connectionChangedTo:reason:) + + diff --git a/assets/code/video-sdk/cloud-proxy/event-handler.mdx b/assets/code/video-sdk/cloud-proxy/event-handler.mdx new file mode 100644 index 000000000..4bdfd6c0f --- /dev/null +++ b/assets/code/video-sdk/cloud-proxy/event-handler.mdx @@ -0,0 +1,103 @@ + + ```kotlin + override fun onProxyConnected(channel: String?, uid: Int, proxyType: Int, + localProxyIp: String?, elapsed: Int) { + // Connected to proxyType + } + ``` + - onProxyConnected + + + ```csharp + public override void OnProxyConnected(string channel, uint uid, PROXY_TYPE proxyType, string localProxyIp, int elapsed) + { + Debug.Log("Cloud proxy service enabled"); + } + ``` + - OnProxyConnected + + + + ```swift + func rtcEngine( + _ engine: AgoraRtcEngineKit, didProxyConnected channel: String, + withUid uid: UInt, proxyType: AgoraProxyType, + localProxyIp: String, elapsed: Int + ) { + // proxy type changed to `proxyType` + self.proxyState = proxyType + } + ``` + + + - rtcEngine(_:didProxyConnected:withUid:proxyType:localProxyIp:elapsed:) + + + - rtcEngine(_:didProxyConnected:withUid:proxyType:localProxyIp:elapsed:) + + + + + ```javascript + const handleVSDKEvents = (eventName, ...args) => { + switch (eventName) { + case "user-published": + if (args[1] == "video") { + // Retrieve the remote video track. + channelParameters.remoteVideoTrack = args[0].videoTrack; + // Retrieve the remote audio track. + channelParameters.remoteAudioTrack = args[0].audioTrack; + // Save the remote user id for reuse. + channelParameters.remoteUid = args[0].uid.toString(); + // Specify the ID of the DIV container. You can use the uid of the remote user. + remotePlayerContainer.id = args[0].uid.toString(); + channelParameters.remoteUid = args[0].uid.toString(); + remotePlayerContainer.textContent = + "Remote user " + args[0].uid.toString(); + // Append the remote container to the page body. + document.body.append(remotePlayerContainer); + // Play the remote video track. + channelParameters.remoteVideoTrack.play(remotePlayerContainer); + } + // Subscribe and play the remote audio track If the remote user publishes the audio track only. + if (args[1] == "audio") { + // Get the RemoteAudioTrack object in the AgoraRTCRemoteUser object. + channelParameters.remoteAudioTrack = args[0].audioTrack; + // Play the remote audio track. No need to pass any DOM element. + channelParameters.remoteAudioTrack.play(); + } + } + }; + ``` + + + ```javascript + const handleVSDKEvents = (eventName, ...args) => { + switch (eventName) { + case "user-published": + // Subscribe and play the remote audio track If the remote user publishes the audio track only. + if (args[1] == "audio") { + // Get the RemoteAudioTrack object in the AgoraRTCRemoteUser object. + channelParameters.remoteAudioTrack = args[0].audioTrack; + // Play the remote audio track. No need to pass any DOM element. + channelParameters.remoteAudioTrack.play(); + } + } + }; + ``` + + + + ```typescript + useClientEvent(agoraEngine, "is-using-cloud-proxy", (isUsingProxy) => { + // Display the proxy server state based on the isUsingProxy Boolean variable. + if (isUsingProxy) { + console.log("Cloud proxy service activated"); + } else { + console.log("Proxy service failed") + } + }); + ``` + - useClientEvent + + diff --git a/assets/code/video-sdk/cloud-proxy/import-library.mdx b/assets/code/video-sdk/cloud-proxy/import-library.mdx new file mode 100644 index 000000000..f8b8c2a07 --- /dev/null +++ b/assets/code/video-sdk/cloud-proxy/import-library.mdx @@ -0,0 +1,28 @@ + + ```kotlin + import io.agora.rtc2.Constants.* + ``` + + + ```swift + import AgoraRtcKit + ``` + + + ```csharp + using Agora.Rtc; + ``` + + + ```javascript + import AgoraManager from "../agora_manager/agora_manager.js"; + ``` + + + ```typescript + import AgoraRTC from "agora-rtc-sdk-ng"; + import { useRTCClient, AgoraRTCProvider, useClientEvent } from "agora-rtc-react"; + import AuthenticationWorkflowManager from "../authentication-workflow/authenticationWorkflowManager"; + import config from "../agora-manager/config"; + ``` + diff --git a/assets/code/video-sdk/cloud-proxy/set-cloud-proxy.mdx b/assets/code/video-sdk/cloud-proxy/set-cloud-proxy.mdx new file mode 100644 index 000000000..95ba5bedb --- /dev/null +++ b/assets/code/video-sdk/cloud-proxy/set-cloud-proxy.mdx @@ -0,0 +1,70 @@ + + ```kotlin + override fun joinChannel(channelName: String, token: String?): Int { + // Check if a proxy connection is required + if (directConnectionFailed) { + // Start cloud proxy service and set automatic UDP mode. + val proxyStatus = agoraEngine!!.setCloudProxy(TRANSPORT_TYPE_UDP_PROXY) + if (proxyStatus == 0) { + sendMessage("Proxy service setup successful") + } else { + sendMessage("Proxy service setup failed with error :$proxyStatus") + } + } + return super.joinChannel(channelName, token) + } + ``` + - setCloudProxy + + + ```swift + func setCloudProxy(to proxyType: AgoraCloudProxyType) { + self.agoraEngine.setCloudProxy(proxyType) + } + ``` + + - setCloudProxy(_:) + + + - setCloudProxy(_:) + + + + ```csharp + agoraEngine.SetCloudProxy(proxyType); + ``` + - SetCloudProxy + - CLOUD_PROXY_TYPE + + + ```javascript + if (config.cloudProxy == true) { + // Start cloud proxy service in the forced UDP mode. + agoraEngine.startProxyServer(3); + agoraEngine.on("is-using-cloud-proxy", (isUsingProxy) => { + // Display the proxy server state based on the isUsingProxy Boolean variable. + if (isUsingProxy == true) { + console.log("Cloud proxy service activated"); + } else { + console.log("Proxy service failed"); + } + }); + } + + const stopProxyServer = () => { + agoraEngine.stopProxyServer(); + } + ``` + - startProxyServer + + + ```typescript + const agoraEngine = useRTCClient(); + useEffect(() => { + agoraEngine.startProxyServer(3); + }, []); + ``` + - useRTCClient + - startProxyServer + + diff --git a/assets/code/video-sdk/cloud-proxy/set-variables.mdx b/assets/code/video-sdk/cloud-proxy/set-variables.mdx new file mode 100644 index 000000000..d7b6c57db --- /dev/null +++ b/assets/code/video-sdk/cloud-proxy/set-variables.mdx @@ -0,0 +1,27 @@ + + Use a variable to keep record of any failed attempts to connect directly. + ```kotlin + boolean directConnectionFailed = false; + ``` + + + ```swift + var proxyState: AgoraProxyType? + ``` + + - AgoraProxyType + + + - AgoraProxyType + + + + ```csharp + private CLOUD_PROXY_TYPE proxyType = CLOUD_PROXY_TYPE.UDP_PROXY; + ``` + + + ```javascript + const agoraEngine = agoraManager.getAgoraEngine(); + ``` + diff --git a/assets/code/video-sdk/custom-video-and-audio/configure-engine-audio.mdx b/assets/code/video-sdk/custom-video-and-audio/configure-engine-audio.mdx new file mode 100644 index 000000000..dd67a0e78 --- /dev/null +++ b/assets/code/video-sdk/custom-video-and-audio/configure-engine-audio.mdx @@ -0,0 +1,20 @@ + +Override the inherited `SetupAgoraEngine` and set an audio profile for optimal audio quality: + ```csharp + public override void SetupAgoraEngine() + { + InitTexture(); + base.SetupAgoraEngine(); + agoraEngine.SetAudioProfile(AUDIO_PROFILE_TYPE.AUDIO_PROFILE_MUSIC_HIGH_QUALITY, + AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT); + SetExternalAudioSource(); + InitEventHandler(); + } + ``` + + - SetAudioProfile + + + - SetAudioProfile + + \ No newline at end of file diff --git a/assets/code/video-sdk/custom-video-and-audio/configure-engine.mdx b/assets/code/video-sdk/custom-video-and-audio/configure-engine.mdx new file mode 100644 index 000000000..b1940b136 --- /dev/null +++ b/assets/code/video-sdk/custom-video-and-audio/configure-engine.mdx @@ -0,0 +1,13 @@ + + ```csharp + // Agora engine setup + public override void SetupAgoraEngine() + { + base.SetupAgoraEngine(); + agoraEngine.SetAudioProfile(AUDIO_PROFILE_TYPE.AUDIO_PROFILE_MUSIC_HIGH_QUALITY, + AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT); + ConfigureExternalAudioSource(); + } + ``` + - SetAudioProfile + \ No newline at end of file diff --git a/assets/code/video-sdk/custom-video-and-audio/create-custom-audio-track.mdx b/assets/code/video-sdk/custom-video-and-audio/create-custom-audio-track.mdx new file mode 100644 index 000000000..8c77d378e --- /dev/null +++ b/assets/code/video-sdk/custom-video-and-audio/create-custom-audio-track.mdx @@ -0,0 +1,37 @@ + + ```typescript + // Create custom audio track using the user's media devices + const createCustomAudioTrack = () => { + navigator.mediaDevices + .getUserMedia({ audio: true }) + .then((stream) => { + const audioMediaStreamTracks = stream.getAudioTracks(); + // For demonstration purposes, we used the default audio device. In a real-time scenario, you can use the dropdown to select the audio device of your choice. + setCustomAudioTrack(AgoraRTC.createCustomAudioTrack({ mediaStreamTrack: audioMediaStreamTracks[0] })); + }).catch((error) => console.error(error)); + }; + useEffect(() => { + if (connectionState === "CONNECTED") { + createCustomAudioTrack(); + } + }, [connectionState]); + ``` + - createCustomAudioTrack + + + ```csharp + private void SetExternalAudioSource() + { + lock (_rtcLock) + { + audioTrackID = agoraEngine.CreateCustomAudioTrack(AUDIO_TRACK_TYPE.AUDIO_TRACK_MIXABLE, new AudioTrackConfig(false)); + } + } + ``` + + - CreateCustomAudioTrack + + + - CreateCustomAudioTrack + + \ No newline at end of file diff --git a/assets/code/video-sdk/custom-video-and-audio/create-custom-video-track.mdx b/assets/code/video-sdk/custom-video-and-audio/create-custom-video-track.mdx new file mode 100644 index 000000000..316244325 --- /dev/null +++ b/assets/code/video-sdk/custom-video-and-audio/create-custom-video-track.mdx @@ -0,0 +1,33 @@ + + ```typescript + // Create custom video track using the user's media devices + const createCustomVideoTrack = () => { + navigator.mediaDevices + .getUserMedia({ video: true }) + .then((stream) => { + const videoMediaStreamTracks = stream.getVideoTracks(); + // For demonstration purposes, we used the default video device. In a real-time scenario, you can use the dropdown to select the video device of your choice. + setCustomVideoTrack(AgoraRTC.createCustomVideoTrack({ mediaStreamTrack: videoMediaStreamTracks[0] })); + }) + .catch((error) => console.error(error)); + }; + useEffect(() => { + if (connectionState === "CONNECTED") { + createCustomVideoTrack(); + } + }, [connectionState]); + ``` + - createCustomVideoTrack + + + ```csharp + // Configure external video source + private void ConfigureExternalVideoSource() + { + Texture2D texture = Resources.Load("agora"); + var ret = agoraEngine.SetExternalVideoSource(true, false, EXTERNAL_VIDEO_SOURCE_TYPE.VIDEO_FRAME, new SenderOptions()); + } + ``` + - SetExternalVideoSource + + \ No newline at end of file diff --git a/assets/code/video-sdk/custom-video-and-audio/destroy-custom-track-audio.mdx b/assets/code/video-sdk/custom-video-and-audio/destroy-custom-track-audio.mdx new file mode 100644 index 000000000..36dff8a5d --- /dev/null +++ b/assets/code/video-sdk/custom-video-and-audio/destroy-custom-track-audio.mdx @@ -0,0 +1,26 @@ + +Override `DestroyEngine` to destroy the track before you destroy the engine: + ```csharp + // Destroy the Agora engine + public override void DestroyEngine() + { + if (audioSource) + { + audioSource.Stop(); + } + if (agoraEngine == null) + { + return; + } + agoraEngine.DestroyCustomAudioTrack(audioTrackID); + base.DestroyEngine(); + } + ``` + + - DestroyCustomAudioTrack + + + - DestroyCustomAudioTrack + + + \ No newline at end of file diff --git a/assets/code/video-sdk/custom-video-and-audio/destroy-custom-track-video.mdx b/assets/code/video-sdk/custom-video-and-audio/destroy-custom-track-video.mdx new file mode 100644 index 000000000..4179d9622 --- /dev/null +++ b/assets/code/video-sdk/custom-video-and-audio/destroy-custom-track-video.mdx @@ -0,0 +1,16 @@ + +Override `DestroyEngine` to destroy the track before you destroy the engine: + ```csharp + public override void DestroyEngine() + { + if (agoraEngine == null) + { + return; + } + agoraEngine.DestroyCustomVideoTrack(videoTrackID); + base.DestroyEngine(); + } + ``` + - DestroyCustomVideoTrack + + \ No newline at end of file diff --git a/assets/code/video-sdk/custom-video-and-audio/enable-audio-publishing.mdx b/assets/code/video-sdk/custom-video-and-audio/enable-audio-publishing.mdx new file mode 100644 index 000000000..773f860f8 --- /dev/null +++ b/assets/code/video-sdk/custom-video-and-audio/enable-audio-publishing.mdx @@ -0,0 +1,103 @@ + + ```kotlin + fun playCustomAudio() { + // Create a custom audio track + val audioTrackConfig = AudioTrackConfig() + audioTrackConfig.enableLocalPlayback = true + + customAudioTrackId = agoraEngine!!.createCustomAudioTrack( + Constants.AudioTrackType.AUDIO_TRACK_MIXABLE, + audioTrackConfig + ) + + // Set custom audio publishing options + val options = ChannelMediaOptions() + options.publishCustomAudioTrack = true // Enable publishing custom audio + options.publishCustomAudioTrackId = customAudioTrackId + options.publishMicrophoneTrack = false // Disable publishing microphone audio + agoraEngine!!.updateChannelMediaOptions(options) + + // Open the audio file + openAudioFile() + + // Start the pushing task + pushingTask = Thread(PushingTask(this)) + pushingAudio = true + pushingTask?.start() + } + + private fun openAudioFile() { + // Open the audio file + try { + inputStream = mContext.resources.assets.open(audioFile) + // Use the inputStream as needed + } catch (e: IOException) { + e.printStackTrace() + } + } + + fun stopCustomAudio() { + pushingAudio = false + pushingTask?.interrupt() + } + ``` + - createCustomAudioTrack + - updateChannelMediaOptions + - setExternalAudioSource + + + + + 1. Call `createcustomaudiotrack` to create a custom audio track and get the audio track ID. + 1. In `AgoraRtcChannelMediaOptions` of your channel, set `publishCustomAduioTrackId` to the audio track ID that you want to publish, and set `publishCustomAudioTrack` to `YES`. + + Add the following lines to the `init()` method of `AgoraManager` to create a custom audio track: + + ```swift + let audioTrackConfig = AgoraAudioTrackConfig() + audioTrackConfig.enableLocalPlayback = false + + self.agoraEngine.createCustomAudioTrack(.direct, config: audioTrackConfig) + ``` + + + - createcustomaudiotrack(_:config:) + + + - createcustomaudiotrack(_:config:) + + + + +```javascript +// Retrieve the available audio tracks. +var audioTracks = stream.getAudioTracks(); +console.log("Using video device: " + audioTracks[0].label); +// Create custom audio track. +channelParameters.localAudioTrack = AgoraRTC.createCustomAudioTrack({ + mediaStreamTrack: audioTracks[0], +}); +``` +- createCustomAudioTrack + + + + ```typescript + const CustomAudioComponent: React.FC<{ customAudioTrack: ILocalAudioTrack | null }> = ({ customAudioTrack }) => { + const agoraContext = useAgoraContext(); + + useEffect(() => { + if (customAudioTrack && agoraContext.localMicrophoneTrack) { + agoraContext.localMicrophoneTrack.stop(); // Stop the default microphone track + customAudioTrack?.play(); // Play the custom audio track for the local user + } + return () => { + customAudioTrack?.stop(); // Stop the custom audio track when the component unmounts + }; + }, [customAudioTrack, agoraContext.localMicrophoneTrack]); + return <>; + }; + ``` + - play + - stop + diff --git a/assets/code/video-sdk/custom-video-and-audio/enable-video-publishing.mdx b/assets/code/video-sdk/custom-video-and-audio/enable-video-publishing.mdx new file mode 100644 index 000000000..31940aa17 --- /dev/null +++ b/assets/code/video-sdk/custom-video-and-audio/enable-video-publishing.mdx @@ -0,0 +1,105 @@ + + ```kotlin + fun setupCustomVideo () { + // Enable publishing of the captured video from a custom source + val options = ChannelMediaOptions() + options.publishCustomVideoTrack = true + options.publishCameraTrack = false + + agoraEngine!!.updateChannelMediaOptions(options) + + // Configure the external video source. + agoraEngine!!.setExternalVideoSource( + true, + true, + Constants.ExternalVideoSourceType.VIDEO_FRAME + ) + + // Check whether texture encoding is supported + sendMessage(if (agoraEngine!!.isTextureEncodeSupported) "Texture encoding is supported" else "Texture encoding is not supported") + } + ``` + - updateChannelMediaOptions + - setExternalVideoSource + - isTextureEncodeSupported + + + ```swift + self.agoraEngine.setExternalVideoSource(true, useTexture: true, sourceType: .videoFrame) + ``` + + + - setExternalVideoSource(_:useTexture:sourceType:) + + + - setExternalVideoSource(_:useTexture:sourceType:) + + + + ```csharp + private void SetExternalVideoSource() + { + var ret = agoraEngine.SetExternalVideoSource(true, false, EXTERNAL_VIDEO_SOURCE_TYPE.VIDEO_FRAME, new SenderOptions()); + videoTrackID = agoraEngine.CreateCustomVideoTrack(); + agoraEngine.DisableVideo(); + LocalView.SetForUser(configData.uid, configData.channelName, VIDEO_SOURCE_TYPE.VIDEO_SOURCE_CUSTOM); + } + private void InitTexture() + { + rect = new Rect(0, 0, Screen.width, Screen.height); + texture = new Texture2D((int)rect.width, (int)rect.height, TextureFormat.RGBA32, false); + } + public void SetVideoEncoderConfiguration() + { + VideoEncoderConfiguration videoEncoderConfiguration = new VideoEncoderConfiguration(); + videoEncoderConfiguration.dimensions = new VideoDimensions((int)rect.width, (int)rect.height); + agoraEngine.SetVideoEncoderConfiguration(videoEncoderConfiguration); + } + ``` + + +```javascript +const startCustomVideoAndAudio = async (channelParameters) => { + await agoraEngine.join( + config.appId, + config.channelName, + config.token, + config.uid + ); + // Create a local audio track from the audio sampled by a microphone. + channelParameters.localAudioTrack = + await AgoraRTC.createMicrophoneAudioTrack(); + + // An object specifying the types of media to request. + var constraints = (window.constraints = { audio: true, video: true }); + // A method to request media stream object. + await navigator.mediaDevices + .getUserMedia(constraints) + .then(function (stream) { + // Get all the available video tracks. + var videoTracks = stream.getVideoTracks(); + console.log("Using video device: " + videoTracks[0].label); + // Create a custom video track. + channelParameters.localVideoTrack = AgoraRTC.createCustomVideoTrack({ + mediaStreamTrack: videoTracks[0], + }); + }) + .catch(function (error) { + console.log(error); + }); + + // Append the local video container to the page body. + document.body.append(channelParameters.localPlayerContainer); + // Publish the local audio and video tracks in the channel. + await agoraEngine.publish([ + channelParameters.localAudioTrack, + channelParameters.localVideoTrack, + ]); + // Play the local video track. + channelParameters.localVideoTrack.play( + channelParameters.localPlayerContainer + ); + }; +``` +- createCustomVideoTrack + diff --git a/assets/code/video-sdk/custom-video-and-audio/import-library-audio.mdx b/assets/code/video-sdk/custom-video-and-audio/import-library-audio.mdx new file mode 100644 index 000000000..be5b1d17e --- /dev/null +++ b/assets/code/video-sdk/custom-video-and-audio/import-library-audio.mdx @@ -0,0 +1,24 @@ + + ```kotlin + import io.agora.rtc2.ChannelMediaOptions + import io.agora.rtc2.Constants + import io.agora.rtc2.audio.AudioTrackConfig + import java.io.IOException + import java.io.InputStream + ``` + + + ```typescript + import { AgoraRTCProvider, useRTCClient, useConnectionState } from "agora-rtc-react"; + import AgoraRTC, {ILocalAudioTrack } from "agora-rtc-sdk-ng"; + import AuthenticationWorkflowManager from "../authentication-workflow/authenticationWorkflowManager"; + import { useAgoraContext } from "../agora-manager/agoraManager"; + import config from "../agora-manager/config"; + ``` + + + ```csharp + using Agora.Rtc; + using RingBuffer; + ``` + \ No newline at end of file diff --git a/assets/code/video-sdk/custom-video-and-audio/import-library.mdx b/assets/code/video-sdk/custom-video-and-audio/import-library.mdx new file mode 100644 index 000000000..b8222ce37 --- /dev/null +++ b/assets/code/video-sdk/custom-video-and-audio/import-library.mdx @@ -0,0 +1,37 @@ + + ```kotlin + import io.agora.base.VideoFrame + import io.agora.rtc2.ChannelMediaOptions + import io.agora.rtc2.Constants + import android.graphics.SurfaceTexture + import android.graphics.SurfaceTexture.OnFrameAvailableListener + import java.io.IOException + import java.io.InputStream + ``` + + + ```swift + import AVKit + import AgoraRtcKit + ``` + + + ```csharp + using Agora.Rtc; + ``` + + +```javascript +import AgoraManager from "../agora_manager/agora_manager.js"; +import AgoraRTC from "agora-rtc-sdk-ng"; +``` + + + ```typescript + import { AgoraRTCProvider, useRTCClient, useConnectionState } from "agora-rtc-react"; + import AgoraRTC, {ILocalVideoTrack } from "agora-rtc-sdk-ng"; + import AuthenticationWorkflowManager from "../authentication-workflow/authenticationWorkflowManager"; + import { useAgoraContext } from "../agora-manager/agoraManager"; + import config from "../agora-manager/config"; + ``` + diff --git a/assets/code/video-sdk/custom-video-and-audio/push-audio-frames.mdx b/assets/code/video-sdk/custom-video-and-audio/push-audio-frames.mdx new file mode 100644 index 000000000..23d938fde --- /dev/null +++ b/assets/code/video-sdk/custom-video-and-audio/push-audio-frames.mdx @@ -0,0 +1,115 @@ + + ```kotlin + internal class PushingTask(private val manager: CustomVideoAudioManager) : Runnable { + override fun run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO) + while (manager.pushingAudio) { + val before = System.currentTimeMillis() + manager.agoraEngine?.pushExternalAudioFrame(manager.readBuffer(), + System.currentTimeMillis(), + manager.sampleRate, + manager.numberOfChannels, + Constants.BytesPerSample.TWO_BYTES_PER_SAMPLE, + manager.customAudioTrackId + ) + val now = System.currentTimeMillis() + val consuming = now - before + if (consuming < manager.pushInterval) { + try { + Thread.sleep(manager.pushInterval - consuming) + } catch (e: InterruptedException) { + e.printStackTrace() + } + } + } + } + } + ``` + - pushExternalAudioFrame + + + ```swift + func audioFrameCaptured(buf: CMSampleBuffer) { + agoraEngine.pushExternalAudioFrameSampleBuffer(buf) + } + ``` + + - pushExternalAudioFrameSampleBuffer(_:) + + + - pushExternalAudioFrameSampleBuffer(_:) + + + + ```csharp + // Coroutine for pushing audio frames + public IEnumerator PushAudioFrameCoroutine() + { + var bytesPerSample = 2; + var type = AUDIO_FRAME_TYPE.FRAME_TYPE_PCM16; + var channels = CHANNEL; + var samples = SAMPLE_RATE / PUSH_FREQ_PER_SEC; + var samplesPerSec = SAMPLE_RATE; + var freq = 1000 / PUSH_FREQ_PER_SEC; + + var audioFrame = new AudioFrame + { + bytesPerSample = BYTES_PER_SAMPLE.TWO_BYTES_PER_SAMPLE, + type = type, + samplesPerChannel = samples, + samplesPerSec = samplesPerSec, + channels = channels, + RawBuffer = new byte[samples * bytesPerSample * CHANNEL], + renderTimeMs = freq + }; + + double startMillisecond = GetTimestamp(); + long tick = 0; + + while (true) + { + yield return null; // Wait for the next frame + + lock (_rtcLock) + { + if (agoraEngine == null) + { + break; + } + + int nRet = -1; + lock (_audioBuffer) + { + if (_audioBuffer.Size > samples * bytesPerSample * CHANNEL) + { + for (var j = 0; j < samples * bytesPerSample * CHANNEL; j++) + { + audioFrame.RawBuffer[j] = _audioBuffer.Get(); + } + nRet = agoraEngine.PushAudioFrame(audioFrame, audioTrackID); + Debug.Log("PushAudioFrame"); + } + } + + if (nRet == 0) + { + tick++; + double nextMillisecond = startMillisecond + tick * freq; + double curMillisecond = GetTimestamp(); + int sleepMillisecond = (int)Math.Ceiling(nextMillisecond - curMillisecond); + if (sleepMillisecond > 0) + { + yield return new WaitForSeconds(sleepMillisecond / 1000.0f); + } + } + } + } + } + ``` + + - PushAudioFrame + + + - PushAudioFrame + + \ No newline at end of file diff --git a/assets/code/video-sdk/custom-video-and-audio/push-video-frames.mdx b/assets/code/video-sdk/custom-video-and-audio/push-video-frames.mdx new file mode 100644 index 000000000..3ad25fbdc --- /dev/null +++ b/assets/code/video-sdk/custom-video-and-audio/push-video-frames.mdx @@ -0,0 +1,88 @@ + + ```kotlin + private val onFrameAvailableListener = OnFrameAvailableListener { + // Callback to notify that a new stream video frame is available. + if (isJoined) { + // Configure the external video frames and send them to the SDK + val videoFrame: VideoFrame? = null + + // Add code here to convert the surfaceTexture data to a VideoFrame object + + // Send VideoFrame to the SDK + agoraEngine!!.pushExternalVideoFrame(videoFrame) + } + } + ``` + - pushExternalVideoFrame + + + ```swift + func myVideoCapture(_ pixelBuffer: CVPixelBuffer, rotation: Int, timeStamp: CMTime) { + let videoFrame = AgoraVideoFrame() + videoFrame.format = 12 + videoFrame.textureBuf = pixelBuffer + videoFrame.time = timeStamp + videoFrame.rotation = Int32(rotation) + + // Push the video frame to the Agora SDK + let framePushed = self.agoraEngine.pushExternalVideoFrame(videoFrame) + if !framePushed { + print("Frame could not be pushed.") + } + } + ``` + + - AgoraVideoFrame + - pushExternalVideoFrame(_:) + + + - AgoraVideoFrame + - pushExternalVideoFrame(_:) + + + + ```csharp + // Coroutine for sharing screen + public IEnumerator ShareScreen() + { + PushAudioFrameCoroutine(); + yield return new WaitForEndOfFrame(); + if (agoraEngine != null) + { + texture.ReadPixels(rect, 0, 0); + texture.Apply(); + + // Check Unity version for texture data access +#if UNITY_2018_1_OR_NEWER + NativeArray nativeByteArray = texture.GetRawTextureData(); + if (shareData?.Length != nativeByteArray.Length) + { + shareData = new byte[nativeByteArray.Length]; + } + nativeByteArray.CopyTo(shareData); +#else + shareData = texture.GetRawTextureData(); +#endif + + ExternalVideoFrame externalVideoFrame = new ExternalVideoFrame(); + externalVideoFrame.type = VIDEO_BUFFER_TYPE.VIDEO_BUFFER_RAW_DATA; + externalVideoFrame.format = VIDEO_PIXEL_FORMAT.VIDEO_PIXEL_RGBA; + externalVideoFrame.buffer = shareData; + externalVideoFrame.stride = (int)rect.width; + externalVideoFrame.height = (int)rect.height; + externalVideoFrame.rotation = 180; + externalVideoFrame.timestamp = DateTime.Now.Ticks / 1000; + var ret = agoraEngine.PushVideoFrame(externalVideoFrame); + Debug.Log("PushVideoFrame ret = " + ret + "time: " + DateTime.Now.Millisecond); + } + } + // Get timestamp millisecond + private double GetTimestamp() + { + TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0); + return ts.TotalMilliseconds; + } + ``` + - PushVideoFrame + + \ No newline at end of file diff --git a/assets/code/video-sdk/custom-video-and-audio/read-audio-input.mdx b/assets/code/video-sdk/custom-video-and-audio/read-audio-input.mdx new file mode 100644 index 000000000..0d8db25dd --- /dev/null +++ b/assets/code/video-sdk/custom-video-and-audio/read-audio-input.mdx @@ -0,0 +1,71 @@ + + ```kotlin + private fun readBuffer(): ByteArray? { + // Read the audio file buffer + val byteSize = bufferSize + val buffer = ByteArray(byteSize) + try { + if (inputStream!!.read(buffer) < 0) { + inputStream!!.reset() + return readBuffer() + } + } catch (e: IOException) { + e.printStackTrace() + } + return buffer + } + ``` + + +1. Dynamically access the audio source attached to the scene and play it: + ```csharp + // Method to play custom audio source + public void PlayCustomAudioSource() + { + GameObject canvas = GameObject.Find("Canvas"); + audioSource = canvas.GetComponent(); + if (audioSource) + { + audioSource.Play(); + } + else + { + Debug.Log("Audio source not found"); + } + } + ``` +1. Listen to the `OnAudioFilterRead` callback and extract the audio source raw data that you pushes in the channel using `StartPushAudioFrame`: + ```csharp + public void OnAudioFilterRead(float[] data, int channels) + { + if (!_startConvertSignal) return; + var rescaleFactor = 32767; + lock (_audioBuffer) + { + foreach (var t in data) + { + var sample = t; + if (sample > 1) sample = 1; + else if (sample < -1) sample = -1; + + var shortData = (short)(sample * rescaleFactor); + var byteArr = new byte[2]; + byteArr = BitConverter.GetBytes(shortData); + + _audioBuffer.Put(byteArr[0]); + _audioBuffer.Put(byteArr[1]); + } + } + } + ``` +1. Stop the audio source upon leaving the channel or quitting the : + ```csharp + private void StopAudioFile() + { + // Find the Canvas GameObject + GameObject canvas = GameObject.Find("Canvas"); + AudioSource audioSource = canvas.GetComponent(); + audioSource.Stop(); + } + ``` + diff --git a/assets/code/video-sdk/custom-video-and-audio/render-custom-video.mdx b/assets/code/video-sdk/custom-video-and-audio/render-custom-video.mdx new file mode 100644 index 000000000..d5417efbb --- /dev/null +++ b/assets/code/video-sdk/custom-video-and-audio/render-custom-video.mdx @@ -0,0 +1,98 @@ + + ```kotlin + fun customLocalVideoPreview(): TextureView { + // Create TextureView + previewTextureView = TextureView(mContext) + // Add a SurfaceTextureListener + previewTextureView.surfaceTextureListener = surfaceTextureListener + + return previewTextureView + } + + private val surfaceTextureListener: SurfaceTextureListener = object : SurfaceTextureListener { + @RequiresApi(Build.VERSION_CODES.O) + override fun onSurfaceTextureAvailable( + surface: SurfaceTexture, + width: Int, + height: Int + ) { + // Invoked when a TextureView's SurfaceTexture is ready for use. + if (mPreviewing) { + // Already previewing custom video + return + } + sendMessage("Surface Texture Available") + mTextureDestroyed = false + + // Set up previewSurfaceTexture + previewSurfaceTexture = SurfaceTexture(true) + previewSurfaceTexture!!.setOnFrameAvailableListener(onFrameAvailableListener) + + // Add code here to: + // * set up and configure the custom video source + // * set SurfaceTexture of the custom video source to previewSurfaceTexture + sendMessage("Add your code to configure a custom video source") + + // Start preview + mPreviewing = true + } + + override fun onSurfaceTextureSizeChanged( + surface: SurfaceTexture, + width: Int, + height: Int + ) { + } + + override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { + mTextureDestroyed = true + return false + } + + override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {} + } + ``` + + + does not support rendering video frames captured in the push mode. You need to implement a custom video renderer using methods from outside the SDK. + + For this, [`AVCaptureDevice`](https://developer.apple.com/documentation/avfoundation/avcapturedevice) and [`AVCaptureSession`](https://developer.apple.com/documentation/avfoundation/avcapturesession) can be used to capture frames and manage capturing sessions. + + Have a look at [`CustomAudioVideoView`](https://github.com/AgoraIO/video-sdk-samples-ios/blob/main/custom-video-and-audio/CustomAudioVideoView.swift) for more details. + + + ```typescript + const CustomVideoComponent: React.FC<{ customVideoTrack: ILocalVideoTrack | null }> = ({ customVideoTrack }) => { + const agoraContext = useAgoraContext(); + useEffect(() => { + if (customVideoTrack && agoraContext.localCameraTrack) { + const mediaStreamTrack = customVideoTrack.getMediaStreamTrack(); + agoraContext.localCameraTrack.replaceTrack(mediaStreamTrack, true) + .then(() => console.log("The default local video track has been changed")) + .catch((error) => { console.log(error) }) + } + return () => { + customVideoTrack?.stop(); // Stop the custom video track when the component unmounts + }; + }, [agoraContext.localCameraTrack, customVideoTrack]); + return <>; + }; + ``` + - getMediaStreamTrack + - replaceTrack + - stop + + + + ```csharp + // Preview the custom video source output + public void PreviewCustomVideoSourceOutput() + { + // Update the VideoSurface component of the local view GameObject. + GameObject localViewGo = LocalView.gameObject; + LocalView = localViewGo.AddComponent(); + // Render the screen sharing track on the local view GameObject. + LocalView.SetForUser(configData.uid, "", VIDEO_SOURCE_TYPE.VIDEO_SOURCE_CUSTOM); + } + ``` + \ No newline at end of file diff --git a/assets/code/video-sdk/custom-video-and-audio/set-variables-audio.mdx b/assets/code/video-sdk/custom-video-and-audio/set-variables-audio.mdx new file mode 100644 index 000000000..fe2f2e125 --- /dev/null +++ b/assets/code/video-sdk/custom-video-and-audio/set-variables-audio.mdx @@ -0,0 +1,40 @@ + + ```kotlin + // Custom audio parameters + private var customAudioTrackId = -1 + private val audioFile = "applause.wav" // raw audio file + private val sampleRate = 44100 + private val numberOfChannels = 2 + private val bitsPerSample = 16 + private val samples = 441 + private val bufferSize = samples * bitsPerSample / 8 * numberOfChannels + private val pushInterval = samples * 1000 / sampleRate + private var inputStream: InputStream? = null + private var pushingTask: Thread? = null + var pushingAudio = false + ``` + + + ```typescript + const [customAudioTrack, setCustomAudioTrack] = useState(null); + const connectionState = useConnectionState(); + const [customMediaState, enableCustomMedia] = useState(false); + ``` + + + ```csharp + // Constants for audio configuration + private const int CHANNEL = 2; + private const int SAMPLE_RATE = 48000; + private const int PUSH_FREQ_PER_SEC = 20; + + // Audio buffer for storing audio data + private RingBuffer _audioBuffer = new RingBuffer(SAMPLE_RATE * CHANNEL, true); + private uint audioTrackID = 0; + + // Flag to control audio conversion process + private bool _startConvertSignal = false; + private System.Object _rtcLock = new System.Object(); + private AudioSource audioSource; + ``` + \ No newline at end of file diff --git a/assets/code/video-sdk/custom-video-and-audio/set-variables.mdx b/assets/code/video-sdk/custom-video-and-audio/set-variables.mdx new file mode 100644 index 000000000..6ec414957 --- /dev/null +++ b/assets/code/video-sdk/custom-video-and-audio/set-variables.mdx @@ -0,0 +1,39 @@ + + ```kotlin + private lateinit var previewTextureView: TextureView + private var previewSurfaceTexture: SurfaceTexture? = null + private var mTextureDestroyed = false + private var mPreviewing = false + ``` + + + ```swift + // The video device being used, for example, the ultra-wide back camera. + var videoCaptureDevice: AVCaptureDevice + // The audio device being used. + var audioCaptureDevice: AVCaptureDevice + // The AVCaptureVideoPreviewLayer that is updated by pushSource whenever a new frame is captured. + // This object is used to populate the local camera frames. + @Published var previewLayer: AVCaptureVideoPreviewLayer? + /// The AgoraCameraSourcePush object responsible for capturing video frames + /// from the capture device and sending it to the delegate, ``CustomAudioVideoManager``. + public var cameraPushSource: AgoraCameraSourcePush? + public var micPushSource: AgoraAudioSourcePush? + ``` + + + ```csharp + // Texture and image variables for video sharing + public Texture2D texture; + private byte[] shareData = null; + private Rect rect; + ``` + + + + ```typescript + const [customVideoTrack, setCustomVideoTrack] = useState(null); + const connectionState = useConnectionState(); + const [customMediaState, enableCustomMedia] = useState(false); + ``` + diff --git a/assets/code/video-sdk/encrypt-media-streams/enable-encryption.mdx b/assets/code/video-sdk/encrypt-media-streams/enable-encryption.mdx new file mode 100644 index 000000000..a9a4961cf --- /dev/null +++ b/assets/code/video-sdk/encrypt-media-streams/enable-encryption.mdx @@ -0,0 +1,149 @@ + To enable media stream encryption in your , create an Encryption Config and specify a key, salt, and encryption mode. + + + Call `enableEncryption` and pass the config as a parameter. + + ```kotlin + fun enableEncryption() { + if (encryptionSaltBase64.isBlank() || encryptionKey.isBlank()) return + // Convert the salt string into bytes + val encryptionSalt: ByteArray = Base64.getDecoder().decode(encryptionSaltBase64) + // An object to specify encryption configuration. + val config = EncryptionConfig() + // Specify an encryption mode. + config.encryptionMode = EncryptionConfig.EncryptionMode.AES_128_GCM2 + // Set encryption key and salt. + config.encryptionKey = encryptionKey + System.arraycopy( + encryptionSalt, + 0, + config.encryptionKdfSalt, + 0, + config.encryptionKdfSalt.size + ) + // Call the method to enable media encryption. + if (agoraEngine!!.enableEncryption(true, config) == 0) { + sendMessage( "Media encryption enabled") + } + } + ``` + - EncryptionConfig + - enableEncryption + + + ```swift + func enableEncryption(key: String, salt: String, mode: AgoraEncryptionMode) { + // Convert the salt string in the Base64 format into bytes + let encryptedSalt = Data( + base64Encoded: salt, options: .ignoreUnknownCharacters + ) + // An object to specify encryption configuration. + let config = AgoraEncryptionConfig() + // Set secret key and salt. + config.encryptionKey = key + config.encryptionKdfSalt = encryptedSalt + // Specify an encryption mode. + config.encryptionMode = mode + // Call the method to enable media encryption. + if agoraEngine.enableEncryption(true, encryptionConfig: config) == 0 { + Task { await self.updateLabel(to: "Media encryption enabled.") } + } else { + Task { await self.updateLabel(to: "Media encryption failed.") } + } + } + ``` + + + - AgoraEncryptionMode + - AgoraEncryptionConfig + - enableEncryption(_:encryptionConfig:) + + + - AgoraEncryptionMode + - AgoraEncryptionConfig + - enableEncryption(_:encryptionConfig:) + + + + ```csharp + void enableEncryption() + { + if (agoraEngine != null) + { + if(configData.encryptionKey == "" || configData.salt == "") + { + Debug.Log("Encryption key or encryption salt were not specified in the config.json file"); + return; + } + // Create an encryption configuration. + var config = new EncryptionConfig + { + // Specify a encryption mode + encryptionMode = ENCRYPTION_MODE.AES_128_GCM2, + // Assign a secret key. + encryptionKey = configData.encryptionKey, + // Assign a salt in Base64 format + encryptionKdfSalt = Convert.FromBase64String(configData.salt) + }; + // Enable the built-in encryption. + agoraEngine.EnableEncryption(true, config); + } + } + ``` + - EnableEncryption + - EncryptionConfig + + + + ```javascript + function base64ToUint8Array(base64Str) { + const raw = window.atob(base64Str); + const result = new Uint8Array(new ArrayBuffer(raw.length)); + for (let i = 0; i < raw.length; i += 1) { + result[i] = raw.charCodeAt(i); + } + return result; + } + + function hex2ascii(hexx) { + const hex = hexx.toString(); //force conversion + let str = ""; + for (let i = 0; i < hex.length; i += 2) { + str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); + } + return str; + } + + // Convert the encryptionSaltBase64 string to base64ToUint8Array. + encryptionSaltBase64 = base64ToUint8Array(config.salt); + // Convert the encryptionKey string to hex2ascii. + encryptionKey = hex2ascii(config.cipherKey); + // Set an encryption mode. + encryptionMode = config.encryptionMode; + + agoraManager + .getAgoraEngine() + .setEncryptionConfig(encryptionMode, encryptionKey, encryptionSaltBase64); + ``` + - EncryptionMode + - setEncryptionConfig + + + ```typescript + const stringToUint8Array = (str: string): Uint8Array => { + const encoder = new TextEncoder(); + return encoder.encode(str); + }; + const useMediaEncryption = () => { + const agoraEngine = useRTCClient(); + useEffect(() => + { + const salt = stringToUint8Array(config.salt); + // Start channel encryption + agoraEngine.setEncryptionConfig(config.encryptionMode, config.cipherKey, salt); + }, []); // Empty dependency array ensures the effect runs only once when the component mounts + }; + ``` + - EncryptionMode + - setEncryptionConfig + diff --git a/assets/code/video-sdk/encrypt-media-streams/enable-end-to-end-encryption.mdx b/assets/code/video-sdk/encrypt-media-streams/enable-end-to-end-encryption.mdx new file mode 100644 index 000000000..402c3d7ed --- /dev/null +++ b/assets/code/video-sdk/encrypt-media-streams/enable-end-to-end-encryption.mdx @@ -0,0 +1,174 @@ + + ```javascript + const joinWithE2EEncryption = async ( + localPlayerContainer, + channelParameters, + password, + uid + ) => { + AgoraRTC.setParameter("ENABLE_ENCODED_TRANSFORM", true); + const token = await fetchToken(uid, config.channelName); + await agoraManager + .getAgoraEngine() + .join(config.appId, config.channelName, token, uid); + // Create a local audio track from the audio sampled by a microphone. + channelParameters.localAudioTrack = + await AgoraRTC.createMicrophoneAudioTrack(); + // Create a local video track from the video captured by a camera. + channelParameters.localVideoTrack = await AgoraRTC.createCameraVideoTrack(); + // Append the local video container to the page body. + document.body.append(localPlayerContainer); + // Publish the local audio and video tracks in the channel. + await agoraManager + .getAgoraEngine() + .publish([ + channelParameters.localAudioTrack, + channelParameters.localVideoTrack, + ]); + const transceiver = + channelParameters.localVideoTrack.getRTCRtpTransceiver(); + if (!transceiver || !transceiver.sender) { + return; + } + const sender = transceiver.sender; + + var browserName = (function (agent) { + switch (true) { + case agent.indexOf("chrome") > -1 && !!window.chrome: + return "Chrome"; + default: + return "other"; + } + })(window.navigator.userAgent.toLowerCase()); + + if (browserName === "Chrome") { + setEncryptionStream(sender, password); + } + // Play the local video track. + channelParameters.localVideoTrack.play(localPlayerContainer); + }; + + async function setEncryptionStream(sender, password) { + const streams = sender.createEncodedStreams(); + const transformer = new TransformStream({ + async transform(chunk, controller) { + // controller.enqueue(chunk); + // return; + + const originView = new Uint8Array(chunk.data); + + let reservedSize = 40; + + const naluType = originView[4] & 0x1f; + console.log(naluType); + if (naluType !== 7) { + reservedSize = 5; + } + + const payload = originView.subarray(reservedSize, originView.length); + const hashKey = await grindKey(password, 10); + const key = await window.crypto.subtle.importKey( + "raw", + hashKey, + { + name: "AES-GCM", + }, + false, + ["encrypt"] + ); + + const iv = await getIv(password); + + const ciphertext = await window.crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128, + }, + key, + payload + ); + + const encryptedView = new Uint8Array( + ciphertext.byteLength + reservedSize + 12 + ); + encryptedView.set(originView.subarray(0, reservedSize)); + encryptedView.set(iv, reservedSize); + encryptedView.set(new Uint8Array(ciphertext), reservedSize + 12); + chunk.data = encryptedView.buffer; + + controller.enqueue(chunk); + }, + }); + + streams.readable.pipeThrough(transformer).pipeTo(streams.writable); + } + + async function setDecryptionStream(receiver, password) { + const streams = receiver.createEncodedStreams(); + const transformer = new TransformStream({ + async transform(chunk, controller) { + // controller.enqueue(chunk); + // return; + + const originView = new Uint8Array(chunk.data); + + let reservedSize = 40; + + const naluType = originView[4] & 0x1f; + if (naluType !== 7) { + // controller.enqueue(chunk); + // return; + reservedSize = 5; + } + + const hashKey = await grindKey(password, 10); + const key = await window.crypto.subtle.importKey( + "raw", + hashKey, + { + name: "AES-GCM", + }, + false, + ["decrypt"] + ); + + const header = originView.subarray(0, reservedSize); + const iv = originView.subarray(reservedSize, reservedSize + 12); + const payload = originView.subarray( + reservedSize + 12, + chunk.data.byteLength + ); + + let decrypted = null; + try { + decrypted = await window.crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128, + }, + key, + payload + ); + } catch (e) { + console.log(e); + controller.enqueue(chunk); + return; + } + + const decryptedView = new Uint8Array( + decrypted.byteLength + reservedSize + ); + decryptedView.set(header); + decryptedView.set(new Uint8Array(decrypted), reservedSize); + chunk.data = decryptedView.buffer; + + controller.enqueue(chunk); + }, + }); + + streams.readable.pipeThrough(transformer).pipeTo(streams.writable); + } + ``` + diff --git a/assets/code/video-sdk/encrypt-media-streams/event-handler.mdx b/assets/code/video-sdk/encrypt-media-streams/event-handler.mdx new file mode 100644 index 000000000..a993d6d52 --- /dev/null +++ b/assets/code/video-sdk/encrypt-media-streams/event-handler.mdx @@ -0,0 +1,56 @@ + + ```csharp + // Event handler class to handle the events raised by Agora's RtcEngine instance + internal class MediaEncryptionEventHandler : UserEventHandler + { + private MediaEncryptionManager encryptionManager; + public MediaEncryptionEventHandler(MediaEncryptionManager manager):base(manager) + { + encryptionManager = manager; + } + public override void OnEncryptionError(RtcConnection connection, ENCRYPTION_ERROR_TYPE errorType) + { + Debug.Log("Encryption error:" + errorType); + } + } + ``` + - OnEncryptionError + + + + ```kotlin + override fun onEncryptionError(errorType: Int) { + Log.d("Encryption error", errorType.toString()) + } + ``` + - onEncryptionError + + + ```typescript + const useCryptError = () => { + const agoraEngine = useRTCClient(); + useClientEvent(agoraEngine,"crypt-error" , () => { + console.log("decryption failed"); + }); + }; + ``` + + + ```swift + public func rtcEngine( + _ engine: AgoraRtcEngineKit, + didOccur errorType: AgoraEncryptionErrorType + ) { + // encryption error handler + } + ``` + + + - rtcEngine(_:didOccur:) + - AgoraEncryptionErrorType + + + - rtcEngine(_:didOccur:) + - AgoraEncryptionErrorType + + \ No newline at end of file diff --git a/assets/code/video-sdk/encrypt-media-streams/import-library.mdx b/assets/code/video-sdk/encrypt-media-streams/import-library.mdx new file mode 100644 index 000000000..c55f28655 --- /dev/null +++ b/assets/code/video-sdk/encrypt-media-streams/import-library.mdx @@ -0,0 +1,25 @@ + + ```kotlin + import io.agora.rtc2.RtcEngine + import io.agora.rtc2.RtcEngineConfig + import io.agora.rtc2.internal.EncryptionConfig + ``` + + + ```swift + import AgoraRtcKit + ``` + + +```javascript +import AgoraManager from "../agora_manager/agora_manager.js"; +``` + + + ```typescript + import { AgoraRTCProvider, useRTCClient, useClientEvent } from 'agora-rtc-react'; + import config from '../agora-manager/config.ts'; + import AuthenticationWorkflowManager from '../authentication-workflow/authenticationWorkflowManager.tsx'; + import AgoraRTC from 'agora-rtc-sdk-ng'; + ``` + diff --git a/assets/code/video-sdk/encrypt-media-streams/set-variables.mdx b/assets/code/video-sdk/encrypt-media-streams/set-variables.mdx new file mode 100644 index 000000000..3f02572ee --- /dev/null +++ b/assets/code/video-sdk/encrypt-media-streams/set-variables.mdx @@ -0,0 +1,18 @@ + + + In a production environment, you retrieve the encryption key and salt from an authentication server. For this code example you generate them locally. + + ```kotlin + private var encryptionKey = "" // A 32-byte string + private var encryptionSaltBase64 = "" // A 32-byte Base64 string + ``` + + +```javascript +// In a production environment, you retrieve the key and salt from + // an authentication server. For this code example you generate locally. + var encryptionKey = ""; + var encryptionSaltBase64 = ""; + var encryptionMode = ""; +``` + diff --git a/assets/code/video-sdk/ensure-channel-quality/event-handler.mdx b/assets/code/video-sdk/ensure-channel-quality/event-handler.mdx new file mode 100644 index 000000000..a7040fb8f --- /dev/null +++ b/assets/code/video-sdk/ensure-channel-quality/event-handler.mdx @@ -0,0 +1,404 @@ + + + ```kotlin + override fun onConnectionStateChanged(state: Int, reason: Int) { + // Occurs when the network connection state changes + sendMessage( + "Connection state changed\n" + + "New state: $state\n" + + "Reason: $reason" + ) + } + + override fun onLastmileQuality(quality: Int) { + // Reports the last-mile network quality of the local user + (mListener as CallQualityManagerListener).onLastMileQuality(quality) + } + + override fun onLastmileProbeResult(result: LastmileProbeResult) { + // Reports the last mile network probe result + agoraEngine!!.stopLastmileProbeTest() + // The result object contains the detailed test results that help you + // manage call quality, for example, the down link bandwidth. + sendMessage("Available down link bandwidth: " + result.downlinkReport.availableBandwidth) + } + + override fun onNetworkQuality(uid: Int, txQuality: Int, rxQuality: Int) { + // Reports the last mile network quality of each user in the channel + (mListener as CallQualityManagerListener).onNetworkQuality( + uid, txQuality, rxQuality + ) + } + + override fun onRtcStats(rtcStats: RtcStats) { + // Reports the statistics of the current session + counter += 1 + var msg = "" + if (counter == 5) msg = + rtcStats.users.toString() + " user(s)" else if (counter == 10) { + msg = "Packet loss rate: " + rtcStats.rxPacketLossRate + counter = 0 + } + if (msg.isNotEmpty()) sendMessage(msg) + } + + override fun onRemoteVideoStateChanged(uid: Int, state: Int, reason: Int, elapsed: Int) { + // Occurs when the remote video stream state changes + val msg = "Remote video state changed:\n" + + "Uid = $uid\n" + + "NewState = $state\n" + + "Reason = $reason\n" + + "Elapsed = $elapsed" + sendMessage(msg) + } + + override fun onRemoteVideoStats(stats: RemoteVideoStats) { + // Reports the statistics of the video stream sent by each remote user + (mListener as CallQualityManagerListener).onRemoteVideoStats( + stats + ) + } + ``` + + - onLocalVideoStats + - onRemoteVideoStats + - onRtcStats + - onNetworkQuality + + + + + ```dart + @override + RtcEngineEventHandler getEventHandler() { + return RtcEngineEventHandler( + // Occurs when the network connection state changes + onConnectionStateChanged: (RtcConnection connection, + ConnectionStateType state, ConnectionChangedReasonType reason) { + messageCallback( + "Connection state changed\n New state: ${state.name}\n Reason: ${reason.name}"); + super.getEventHandler().onConnectionStateChanged!( + connection, state, reason); + }, + // Reports the last-mile network quality of the local user + onLastmileQuality: (QualityType quality) { + networkQuality = quality.index; + Map eventArgs = {}; + eventArgs["quality"] = quality; + eventCallback("onLastmileQuality", eventArgs); + }, + // Reports the last mile network probe test result + onLastmileProbeResult: (LastmileProbeResult result) { + agoraEngine!.stopLastmileProbeTest(); + // The result object contains the detailed test results that help you + // manage call quality, for example, the down link jitter. + messageCallback("Downlink jitter: ${result.downlinkReport?.jitter}"); + }, + // Reports the last mile network quality of each user in the channel + onNetworkQuality: (RtcConnection connection, int remoteUid, + QualityType txQuality, QualityType rxQuality) { + // Use downlink network quality to update the network status + networkQuality = rxQuality.index; + + Map eventArgs = {}; + eventArgs["connection"] = connection; + eventArgs["remoteUid"] = remoteUid; + eventArgs["txQuality"] = txQuality; + eventArgs["rxQuality"] = rxQuality; + eventCallback("onNetworkQuality", eventArgs); + }, + // Reports the statistics of the current call + onRtcStats: (RtcConnection connection, RtcStats stats) { + counter += 1; + String msg = ""; + + if (counter == 5) { + msg = "${stats.userCount} user(s)"; + } else if (counter == 10) { + msg = "Last mile delay: ${stats.lastmileDelay}"; + counter = 0; + } + if (msg.isNotEmpty) messageCallback(msg); + }, + // Occurs when the remote video stream state changes + onRemoteVideoStateChanged: (RtcConnection connection, int remoteUid, + RemoteVideoState state, RemoteVideoStateReason reason, int elapsed) { + String msg = "Remote video state changed: \n Uid: $remoteUid" + " \n NewState: $state\n reason: $reason\n elapsed: $elapsed"; + messageCallback(msg); + }, + // Reports the statistics of the video stream sent by each remote user + onRemoteVideoStats: (RtcConnection connection, RemoteVideoStats stats) { + remoteVideoStatsSummary = "Uid: ${stats.uid}" + "\nRenderer frame rate: ${stats.rendererOutputFrameRate}" + "\nReceived bitrate: ${stats.receivedBitrate}" + "\nPublish duration: ${stats.publishDuration}" + "\nFrame loss rate: ${stats.frameLossRate}"; + + Map eventArgs = {}; + eventArgs["connection"] = connection; + eventArgs["stats"] = stats; + eventCallback("onRemoteVideoStats", eventArgs); + }, + onTokenPrivilegeWillExpire: (RtcConnection connection, String token) { + super.getEventHandler().onTokenPrivilegeWillExpire!(connection, token); + }, + onJoinChannelSuccess: (RtcConnection connection, int elapsed) { + if (connection.localUid == 0xFFFFFFFF) { + // Echo test started + messageCallback("Audio echo test started"); + return; + } else { + // Joined a channel + isJoined = true; + } + messageCallback( + "Local user uid:${connection.localUid} joined the channel"); + Map eventArgs = {}; + eventArgs["connection"] = connection; + eventArgs["elapsed"] = elapsed; + eventCallback("onJoinChannelSuccess", eventArgs); + super.getEventHandler().onJoinChannelSuccess!(connection, elapsed); + }, + onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) { + super.getEventHandler().onUserJoined!(connection, remoteUid, elapsed); + }, + onUserOffline: (RtcConnection connection, int remoteUid, + UserOfflineReasonType reason) { + super.getEventHandler().onUserOffline!(connection, remoteUid, reason); + }, + ); + } + ``` + + + ```swift + public func rtcEngine(_ engine: AgoraRtcEngineKit, lastmileProbeTest result: AgoraLastmileProbeResult + ) { + engine.stopLastmileProbeTest() + // The result object contains the detailed test results that help you + // manage call quality. For example, the downlink jitter" + print("downlink jitter: \(result.downlinkReport.jitter)") + } + + public func rtcEngine(_ engine: AgoraRtcEngineKit, remoteVideoStats stats: AgoraRtcRemoteVideoStats) { + self.callQualities[stats.uid] = """ + Received Bitrate = \(stats.receivedBitrate) + Frame = \(stats.width)x\(stats.height), \(stats.receivedFrameRate)fps + Frame Loss Rate = \(stats.frameLossRate) + Packet Loss Rate = \(stats.packetLossRate) + """ + } + + public func rtcEngine( + _ engine: AgoraRtcEngineKit, localVideoStats stats: AgoraRtcLocalVideoStats, + sourceType: AgoraVideoSourceType + ) { + self.callQualities[self.localUserId] = """ + Captured Frame = \(stats.captureFrameWidth)x\(stats.captureFrameHeight), \(stats.captureFrameRate)fps + Encoded Frame = \(stats.encodedFrameWidth)x\(stats.encodedFrameHeight), \(stats.encoderOutputFrameRate)fps + Sent Data = \(stats.sentFrameRate)fps, bitrate: \(stats.sentBitrate) + Packet Loss Rate = \(stats.txPacketLossRate) + """ + } + ``` + + + - rtcEngine(_:lastmileProbeTest:) + - rtcEngine(_:remoteVideoStats:) + - rtcEngine(_:localVideoStats:sourceType:) + + + - rtcEngine(_:lastmileProbeTest:) + - rtcEngine(_:remoteVideoStats:) + - rtcEngine(_:localVideoStats:sourceType:) + + + + + +```csharp +// Event handler class to handle the events raised by Agora's RtcEngine instance +internal class CallQualityEventHandler : UserEventHandler +{ + private CallQualityManager callQuality; + internal CallQualityEventHandler(CallQualityManager audioSample):base(audioSample) + { + callQuality = audioSample; + } + public override void OnConnectionStateChanged(RtcConnection connection, CONNECTION_STATE_TYPE state, CONNECTION_CHANGED_REASON_TYPE reason) + { + Debug.Log("Connection state changed" + + "\n New state: " + state + + "\n Reason: " + reason); + } + public override void OnLastmileQuality(int quality) + { + callQuality.updateNetworkStatus(quality); + } + public override void OnLastmileProbeResult(LastmileProbeResult result) + { + callQuality.agoraEngine.StopLastmileProbeTest(); + + Debug.Log("Probe test finished"); + // The result object contains the detailed test results that help you + // manage call quality, for example, the downlink jitter. + Debug.Log("Downlink jitter: " + result.downlinkReport.jitter); + + //Destroy the engine + callQuality.DestroyEngine(); + + } + public override void OnNetworkQuality(RtcConnection connection, uint remoteUid, int txQuality, int rxQuality) + { + // Use downlink network quality to update the network status + callQuality.updateNetworkStatus(rxQuality); + } + public override void OnRtcStats(RtcConnection connection, RtcStats rtcStats) + { + string msg = ""; + msg = rtcStats.userCount + " user(s)"; + msg = "Packet loss rate: " + rtcStats.rxPacketLossRate; + Debug.Log(msg); + } +} +``` +- OnConnectionStateChanged +- OnLastmileQuality +- OnLastmileProbeResult +- OnNetworkQuality +- OnRtcStats + + + +```csharp +// Event handler class to handle the events raised by Agora's RtcEngine instance +internal class CallQualityEventHandler : UserEventHandler +{ + private CallQualityManager callQuality; + internal CallQualityEventHandler(CallQualityManager videoSample):base(videoSample) + { + callQuality = videoSample; + } + public override void OnConnectionStateChanged(RtcConnection connection, CONNECTION_STATE_TYPE state, CONNECTION_CHANGED_REASON_TYPE reason) + { + Debug.Log("Connection state changed" + + "\n New state: " + state + + "\n Reason: " + reason); + } + public override void OnLastmileQuality(int quality) + { + callQuality.updateNetworkStatus(quality); + } + public override void OnLastmileProbeResult(LastmileProbeResult result) + { + callQuality.agoraEngine.StopLastmileProbeTest(); + + Debug.Log("Probe test finished"); + // The result object contains the detailed test results that help you + // manage call quality, for example, the downlink jitter. + Debug.Log("Downlink jitter: " + result.downlinkReport.jitter); + + //Destroy the engine + callQuality.DestroyEngine(); + + } + public override void OnNetworkQuality(RtcConnection connection, uint remoteUid, int txQuality, int rxQuality) + { + // Use downlink network quality to update the network status + callQuality.updateNetworkStatus(rxQuality); + } + public override void OnRtcStats(RtcConnection connection, RtcStats rtcStats) + { + string msg = ""; + msg = rtcStats.userCount + " user(s)"; + msg = "Packet loss rate: " + rtcStats.rxPacketLossRate; + Debug.Log(msg); + } + public override void OnRemoteVideoStateChanged(RtcConnection connection, uint remoteUid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) + { + string msg = "Remote video state changed: \n Uid =" + remoteUid + + " \n NewState =" + state + + " \n reason =" + reason + + " \n elapsed =" + elapsed; + Debug.Log(msg); + } + public override void OnRemoteVideoStats(RtcConnection connection, RemoteVideoStats stats) + { + string msg = "Remote Video Stats: " + + "\n User id =" + stats.uid + + "\n Received bitrate =" + stats.receivedBitrate + + "\n Total frozen time =" + stats.totalFrozenTime; + Debug.Log(msg); + } + +} +``` + - OnConnectionStateChanged + - OnLastmileQuality + - OnLastmileProbeResult + - OnNetworkQuality + - OnRtcStats + - OnRemoteVideoStateChanged + - OnRemoteVideoStats + + + + + ```typescript + const networkQuality = useNetworkQuality(); + const connectionState = useConnectionState(); + ``` + - useNetworkQuality + - useConnectionState + + + + ```javascript + // Get the uplink network condition + agoraEngine.on("network-quality", (quality) => { + if (quality.uplinkNetworkQuality == 1) { + document.getElementById("upLinkIndicator").innerHTML = "Excellent"; + document.getElementById("upLinkIndicator").style.color = "green"; + } else if (quality.uplinkNetworkQuality == 2) { + document.getElementById("upLinkIndicator").innerHTML = "Good"; + document.getElementById("upLinkIndicator").style.color = "yellow"; + } else quality.uplinkNetworkQuality >= 4; + { + document.getElementById("upLinkIndicator").innerHTML = "Poor"; + document.getElementById("upLinkIndicator").style.color = "red"; + } + }); + + // Get the downlink network condition + agoraEngine.on("network-quality", (quality) => { + if (quality.downlinkNetworkQuality == 1) { + document.getElementById("downLinkIndicator").innerHTML = "Excellent"; + document.getElementById("downLinkIndicator").style.color = "green"; + } else if (quality.downlinkNetworkQuality == 2) { + document.getElementById("downLinkIndicator").innerHTML = "Good"; + document.getElementById("downLinkIndicator").style.color = "yellow"; + } else if (quality.downlinkNetworkQuality >= 4) { + document.getElementById("downLinkIndicator").innerHTML = "Poor"; + document.getElementById("downLinkIndicator").style.color = "red"; + } + }); + + const handleVSDKEvents = (eventName, ...args) => { + switch (eventName) { + // ... + case "connection-state-change": + // The sample code uses debug console to show the connection state. In a real-world application, you can add + // a label or a icon to the user interface to show the connection state. + + // Display the current connection state. + console.log("Connection state has changed to :" + args[0]); + // Display the previous connection state. + console.log("Connection state was : " + args[1]); + // Display the connection state change reason. + console.log("Connection state change reason : " + args[2]); + } + }; + ``` + - network-quality + diff --git a/assets/code/video-sdk/ensure-channel-quality/implement-call-quality-view.mdx b/assets/code/video-sdk/ensure-channel-quality/implement-call-quality-view.mdx new file mode 100644 index 000000000..d7c476fe2 --- /dev/null +++ b/assets/code/video-sdk/ensure-channel-quality/implement-call-quality-view.mdx @@ -0,0 +1,52 @@ + + ```swift + public override func setupEngine() -> AgoraRtcEngineKit { + let engine = super.setupEngine() + + // Set Audio Scenario + engine.setAudioScenario(.gameStreaming) + + // Enable dual stream mode + engine.setDualStreamMode(.enableSimulcastStream) + engine.setAudioProfile(.default) + + // Set the video configuration + let videoConfig = AgoraVideoEncoderConfiguration( + size: CGSize(width: 640, height: 360), + frameRate: .fps10, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative, + mirrorMode: .auto + ) + engine.setVideoEncoderConfiguration(videoConfig) + + return engine + } + ``` + + + ```swift + public override func setupEngine() -> AgoraRtcEngineKit { + let engine = super.setupEngine() + + // Set Audio Scenario + engine.setAudioScenario(.gameStreaming) + + // Enable dual stream mode + engine.setDualStreamMode(.enableSimulcastStream) + engine.setAudioProfile(.default) + + // Set the video configuration + let videoConfig = AgoraVideoEncoderConfiguration( + size: CGSize(width: 640, height: 360), + frameRate: .fps10, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative, + mirrorMode: .auto + ) + engine.setVideoEncoderConfiguration(videoConfig) + + return engine + } + ``` + \ No newline at end of file diff --git a/assets/code/video-sdk/ensure-channel-quality/implement-declarations.mdx b/assets/code/video-sdk/ensure-channel-quality/implement-declarations.mdx new file mode 100644 index 000000000..3cc92bd8f --- /dev/null +++ b/assets/code/video-sdk/ensure-channel-quality/implement-declarations.mdx @@ -0,0 +1,121 @@ + + ```kotlin + private val baseEventHandler: IRtcEngineEventHandler + ``` + + + ```swift + @Published public var callQualities: [UInt: String] = [:] + ``` + + + + ```csharp + private IAudioDeviceManager _audioDeviceManager; // To manage audio devices. + private IVideoDeviceManager _videoDeviceManager; // To manage video devices. + private DeviceInfo[] _audioRecordingDeviceInfos; // Represent information about audio recording devices. + private DeviceInfo[] _videoDeviceInfos; // Represent information about video devices. + + public string networkStatus = ""; + public List videoDevices; + public List audioDevices; + + [DllImport("user32.dll")] + private static extern IntPtr CreateWindowEx(uint dwExStyle, string lpClassName, string lpWindowName, + uint dwStyle, + int x, int y, + int nWidth, + int nHeight, + IntPtr hWndParent, + IntPtr hMenu, + IntPtr hInstance, + IntPtr lpParam); + + [DllImport("user32.dll")] + private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("user32.dll")] + private static extern bool DestroyWindow(IntPtr hWnd); + + private const uint WS_OVERLAPPEDWINDOW = 0x00CF0000; + private const uint WS_VISIBLE = 0x10000000; + private const int SW_SHOW = 5; + private IntPtr hWnd; + ``` + + + ```csharp + private IAudioDeviceManager _audioDeviceManager; // To manage audio devices. + private DeviceInfo[] _audioRecordingDeviceInfos; // Represent information about audio recording devices. + + public string networkStatus = ""; + public List audioDevices; + ``` + + + + + + ```javascript + // A variable to track the state of device test. + var isDeviceTestRunning = false; + // Variables to hold the Audio tracks for device testing. + var testTracks; + // A variable to reference the audio devices dropdown. + var audioDevicesDropDown; + + let channelParameters = { + // A variable to hold a local audio track. + localAudioTrack: null, + // A variable to hold a remote audio track. + remoteAudioTrack: null, + // A variable to hold the remote user id.s + remoteUid: "1", + }; + ``` + + + ```javascript + // A variable to track the state of remote video quality. + var isHighRemoteVideoQuality = false; + // A variable to track the state of device test. + var isDeviceTestRunning = false; + // Variables to hold the Audio/Video tracks for device testing. + var testTracks; + // A variable to reference the audio devices dropdown. + var audioDevicesDropDown; + // A variable to reference the video devices dropdown. + var videoDevicesDropDown; + + let channelParameters = { + // A variable to hold a local audio track. + localAudioTrack: null, + // A variable to hold a local video track. + localVideoTrack: null, + // A variable to hold a remote audio track. + remoteAudioTrack: null, + // A variable to hold a remote video track. + remoteVideoTrack: null, + // A variable to hold the remote user id.s + remoteUid: "1", + }; + ``` + + + + ```typescript + const agoraEngine = useRTCClient(); + const remoteUsers = useRemoteUsers(); + const [isHighRemoteVideoQuality, setVideoQualityState] = useState(false); + const numberOfRemoteUsers = remoteUsers.length; + const remoteUser = remoteUsers[numberOfRemoteUsers - 1]; + const [isDeviceTestRunning, setDeviceTestState] = useState(false); + const { localMicrophoneTrack } = useLocalMicrophoneTrack(); + const { localCameraTrack } = useLocalCameraTrack(); + const enabledFeatures = useRef(false); + ``` + - useRTCClient + - useRemoteUsers + - useLocalCameraTrack + - useLocalMicrophoneTrack + diff --git a/assets/code/video-sdk/ensure-channel-quality/swift/implement-labels.mdx b/assets/code/video-sdk/ensure-channel-quality/implement-labels.mdx similarity index 100% rename from assets/code/video-sdk/ensure-channel-quality/swift/implement-labels.mdx rename to assets/code/video-sdk/ensure-channel-quality/implement-labels.mdx diff --git a/assets/code/video-sdk/ensure-channel-quality/swift/implement-network-status.mdx b/assets/code/video-sdk/ensure-channel-quality/implement-network-status.mdx similarity index 62% rename from assets/code/video-sdk/ensure-channel-quality/swift/implement-network-status.mdx rename to assets/code/video-sdk/ensure-channel-quality/implement-network-status.mdx index 6702d036f..0bb877085 100644 --- a/assets/code/video-sdk/ensure-channel-quality/swift/implement-network-status.mdx +++ b/assets/code/video-sdk/ensure-channel-quality/implement-network-status.mdx @@ -17,4 +17,23 @@ func updateNetworkStatus(quality: Int) { else { networkStatus.backgroundColor = UIColor.white } } ``` -
\ No newline at end of file +
+ + ```csharp + public void updateNetworkStatus(int quality) + { + if (quality > 0 && quality < 3) + { + networkStatus = "Network Quality: Perfect"; + } + else if (quality <= 4) + { + networkStatus = "Network Quality: Good"; + } + else if (quality <= 6) + { + networkStatus = "Network Quality: Poor"; + } + } + ``` + diff --git a/assets/code/video-sdk/ensure-channel-quality/import-library.mdx b/assets/code/video-sdk/ensure-channel-quality/import-library.mdx new file mode 100644 index 000000000..68b4430cd --- /dev/null +++ b/assets/code/video-sdk/ensure-channel-quality/import-library.mdx @@ -0,0 +1,57 @@ + + ```kotlin + import io.agora.rtc2.* + import io.agora.rtc2.video.VideoCanvas + import io.agora.rtc2.internal.LastmileProbeConfig + import io.agora.rtc2.video.VideoEncoderConfiguration + import io.agora.rtc2.IRtcEngineEventHandler.RemoteVideoStats + ``` + + + ```dart + import 'package:agora_rtc_engine/agora_rtc_engine.dart'; + import 'package:permission_handler/permission_handler.dart'; + import 'package:flutter_reference_app/authentication-workflow/agora_manager_authentication.dart'; + import 'package:flutter_reference_app/agora-manager/agora_manager.dart'; + ``` + + + ```swift + import SwiftUI + import AgoraRtcKit + ``` + + + ```csharp + using Agora.Rtc; + using System.Runtime.InteropServices; + using System; + ``` + + +```javascript +import AgoraManager from "../agora_manager/agora_manager.js"; +import AgoraRTC from "agora-rtc-sdk-ng"; +``` + + + ```typescript + import { + AgoraRTCProvider, + useRTCClient, + useRemoteUsers, + useLocalCameraTrack, + useLocalMicrophoneTrack, + useNetworkQuality, + useConnectionState, + useJoin, + LocalVideoTrack, + useAutoPlayAudioTrack, + useVolumeLevel + } from "agora-rtc-react"; + import AgoraRTC, {ILocalAudioTrack, ICameraVideoTrack} from "agora-rtc-sdk-ng"; + import AuthenticationWorkflowManager from "../authentication-workflow/authenticationWorkflowManager"; + import { useState, useRef, useEffect } from "react"; + import config from "../agora-manager/config"; + ``` + diff --git a/assets/code/video-sdk/ensure-channel-quality/probe-test.mdx b/assets/code/video-sdk/ensure-channel-quality/probe-test.mdx new file mode 100644 index 000000000..5fb7d397c --- /dev/null +++ b/assets/code/video-sdk/ensure-channel-quality/probe-test.mdx @@ -0,0 +1,125 @@ + + + ```kotlin + private fun startProbeTest() { + if (agoraEngine == null) setupAgoraEngine() + // Configure a LastmileProbeConfig instance. + val config = LastmileProbeConfig() + // Probe the uplink network quality. + config.probeUplink = true + // Probe the down link network quality. + config.probeDownlink = true + // The expected uplink bitrate (bps). The value range is [100000,5000000]. + config.expectedUplinkBitrate = 100000 + // The expected down link bitrate (bps). The value range is [100000,5000000]. + config.expectedDownlinkBitrate = 100000 + agoraEngine!!.startLastmileProbeTest(config) + sendMessage("Running the last mile probe test ...") + // Test results are reported through the onLastmileProbeResult callback + } + ``` + + - LastmileProbeConfig + + - startLastmileProbeTest + + + + ```dart + void startProbeTest() { + // Configure the probe test + LastmileProbeConfig config = const LastmileProbeConfig( + probeUplink: true, + probeDownlink: true, + expectedUplinkBitrate: 100000, // Range 100000-5000000 bps + expectedDownlinkBitrate: 100000, // Range 100000-5000000 bps + ); + agoraEngine!.startLastmileProbeTest(config); + messageCallback("Running the last mile probe test ..."); + // Test results are reported through the onLastmileProbeResult callback + } + ``` + + + ```swift + func startProbeTest() { + // Configure a LastmileProbeConfig instance. + let config = AgoraLastmileProbeConfig() + // Probe the uplink network quality. + config.probeUplink = true + // Probe the downlink network quality. + config.probeDownlink = true + // The expected uplink bitrate (bps). The value range is [100000,5000000]. + config.expectedUplinkBitrate = 100000 + // The expected downlink bitrate (bps). The value range is [100000,5000000]. + config.expectedDownlinkBitrate = 100000 + + print(agoraEngine.startLastmileProbeTest(config)) + } + + // Result of the probe test + public func rtcEngine( + _ engine: AgoraRtcEngineKit, + lastmileQuality quality: AgoraNetworkQuality + ) { + self.lastMileQuality = quality + } + ``` + + + - AgoraLastmileProbeConfig + - startLastmileProbeTest(_:) + - rtcEngine(_:lastmileQuality:) + + + - AgoraLastmileProbeConfig + - startLastmileProbeTest(_:) + - rtcEngine(_:lastmileQuality:) + + + + ```csharp + // Probe test to check network quality. + public void StartProbeTest() + { + // Configure a LastmileProbeConfig instance. + LastmileProbeConfig config = new LastmileProbeConfig(); + + // Probe theuplink network quality. + config.probeUplink = true; + + // Probe the downlink network quality. + config.probeDownlink = true; + + // The expected uplink bitrate (bps). The value range is [100000,5000000]. + config.expectedUplinkBitrate = 100000; + + // The expected downlink bitrate (bps). The value range is [100000,5000000]. + config.expectedDownlinkBitrate = 100000; + + agoraEngine.StartLastmileProbeTest(config); + Debug.Log("Running the last mile probe test ..."); + } + ``` + + - StartLastmileProbeTest + - LastmileProbeConfig + + + - StartLastmileProbeTest + - LastmileProbeConfig + + + + ```typescript + const updateNetworkStatus = () => { + const networkLabels = { + 0: 'Unknown', 1: 'Excellent', + 2: 'Good', 3: 'Poor', + 4: 'Bad', 5: 'Very Bad', + 6: 'No Connection' + } + return ; + }; + ``` + \ No newline at end of file diff --git a/assets/code/video-sdk/ensure-channel-quality/set-audio-video-profile.mdx b/assets/code/video-sdk/ensure-channel-quality/set-audio-video-profile.mdx new file mode 100644 index 000000000..1b2ebbd63 --- /dev/null +++ b/assets/code/video-sdk/ensure-channel-quality/set-audio-video-profile.mdx @@ -0,0 +1,28 @@ + + ```js + const setAudioProfile = async () => { + // Create a local audio track and set an audio profile for the local audio track. + channelParameters.localAudioTrack = + await AgoraRTC.createMicrophoneAudioTrack({ + encoderConfig: "high_quality_stereo", + }); + }; + + const setVideoProfile = async () => { + // Set a video profile. + channelParameters.localVideoTrack = await AgoraRTC.createCameraVideoTrack({ + optimizationMode: "detail", + encoderConfig: { + width: 640, + // Specify a value range and an ideal value + height: { ideal: 480, min: 400, max: 500 }, + frameRate: 15, + bitrateMin: 600, + bitrateMax: 1000, + }, + }); + }; + ``` + - encoderConfig + - optimizationMode + diff --git a/assets/code/video-sdk/ensure-channel-quality/set-latency.mdx b/assets/code/video-sdk/ensure-channel-quality/set-latency.mdx new file mode 100644 index 000000000..0fd57c7c7 --- /dev/null +++ b/assets/code/video-sdk/ensure-channel-quality/set-latency.mdx @@ -0,0 +1,65 @@ + + + ```kotlin + // Set the latency level + options.audienceLatencyLevel = Constants.AUDIENCE_LATENCY_LEVEL_ULTRA_LOW_LATENCY; + ``` + + + ```kotlin + // Set the latency level + options.audienceLatencyLevel = Constants.AUDIENCE_LATENCY_LEVEL_LOW_LATENCY; + ``` + + - ChannelMediaOptions + + + + ```dart + // Set the latency level + ChannelMediaOptions options = ChannelMediaOptions( + clientRoleType: clientRole, + channelProfile: ChannelProfileType.channelProfileLiveBroadcasting, + audienceLatencyLevel: AudienceLatencyLevelType.audienceLatencyLevelUltraLowLatency + ); + ``` + + + ```dart + // Set the latency level + ChannelMediaOptions options = ChannelMediaOptions( + clientRoleType: clientRole, + channelProfile: ChannelProfileType.channelProfileLiveBroadcasting, + audienceLatencyLevel: AudienceLatencyLevelType.audienceLatencyLevelLowLatency + ); + ``` + + + + + ```swift + let opt = AgoraRtcChannelMediaOptions() + opt.audienceLatencyLevel = .ultraLowLatency + ``` + + + ```swift + let opt = AgoraRtcChannelMediaOptions() + opt.audienceLatencyLevel = .lowLatency + ``` + + + - audienceLatencyLevel + - AgoraAudienceLatencyLevelType + + + - audienceLatencyLevel + - AgoraAudienceLatencyLevelType + + + + ``` csharp + // Set the latency level + options.audienceLatencyLevel= Constants.AUDIENCE_LATENCY_LEVEL_ULTRA_LOW_LATENCY; + ``` + \ No newline at end of file diff --git a/assets/code/video-sdk/ensure-channel-quality/setup-engine.mdx b/assets/code/video-sdk/ensure-channel-quality/setup-engine.mdx new file mode 100644 index 000000000..7951a04bc --- /dev/null +++ b/assets/code/video-sdk/ensure-channel-quality/setup-engine.mdx @@ -0,0 +1,367 @@ + + + ```kotlin + override fun setupAgoraEngine(): Boolean { + try { + val config = RtcEngineConfig() + config.mContext = mContext + config.mAppId = appId + config.mEventHandler = iRtcEngineEventHandler + // Configure the log file + val logConfig = RtcEngineConfig.LogConfig() + logConfig.fileSizeInKB = 256 // Range 128-1024 Kb + logConfig.level = Constants.LogLevel.getValue(Constants.LogLevel.LOG_LEVEL_WARN) + config.mLogConfig = logConfig + agoraEngine = RtcEngine.create(config) + // Enable video mode + agoraEngine!!.enableVideo() + } catch (e: Exception) { + sendMessage(e.toString()) + return false + } + + // Enable the dual stream mode + agoraEngine!!.setDualStreamMode(Constants.SimulcastStreamMode.ENABLE_SIMULCAST_STREAM) + // If you set the dual stream mode to AUTO_SIMULCAST_STREAM, the low-quality video + // steam is not sent by default; the SDK automatically switches to low-quality after + // it receives a request to subscribe to a low-quality video stream. + + // Set an audio profile and an audio scenario. + agoraEngine!!.setAudioProfile( + Constants.AUDIO_PROFILE_DEFAULT, + Constants.AUDIO_SCENARIO_GAME_STREAMING + ) + + // Set the video profile + val videoConfig = VideoEncoderConfiguration() + // Set mirror mode + videoConfig.mirrorMode = VideoEncoderConfiguration.MIRROR_MODE_TYPE.MIRROR_MODE_AUTO + // Set frameRate + videoConfig.frameRate = VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_10.value + // Set bitrate + videoConfig.bitrate = VideoEncoderConfiguration.STANDARD_BITRATE + // Set dimensions + videoConfig.dimensions = VideoEncoderConfiguration.VD_640x360 + // Set orientation mode + videoConfig.orientationMode = + VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE + // Set degradation preference + videoConfig.degradationPrefer = + VideoEncoderConfiguration.DEGRADATION_PREFERENCE.MAINTAIN_BALANCED + // Set compression preference: low latency or quality + videoConfig.advanceOptions.compressionPreference = + VideoEncoderConfiguration.COMPRESSION_PREFERENCE.PREFER_LOW_LATENCY + // Apply the configuration + agoraEngine!!.setVideoEncoderConfiguration(videoConfig) + + // Start the probe test + startProbeTest() + return true + } + ``` + + - setAudioProfile + - enableDualStreamMode + - VideoEncoderConfiguration + - setVideoEncoderConfiguration + - LogConfig + + + + ```kotlin + override fun setupAgoraEngine(): Boolean { + try { + val config = RtcEngineConfig() + config.mContext = mContext + config.mAppId = appId + config.mEventHandler = iRtcEngineEventHandler + // Configure the log file + val logConfig = RtcEngineConfig.LogConfig() + logConfig.fileSizeInKB = 256 // Range 128-1024 Kb + logConfig.level = Constants.LogLevel.getValue(Constants.LogLevel.LOG_LEVEL_WARN) + config.mLogConfig = logConfig + agoraEngine = RtcEngine.create(config) + } catch (e: Exception) { + sendMessage(e.toString()) + return false + } + + // Set an audio profile and an audio scenario. + agoraEngine!!.setAudioProfile( + Constants.AUDIO_PROFILE_DEFAULT, + Constants.AUDIO_SCENARIO_GAME_STREAMING + ) + + // Start the probe test + startProbeTest() + return true } + ``` + + - setAudioProfile + - LogConfig + + + + + + ```dart + @override + Future setupAgoraEngine() async { + // Retrieve or request camera and microphone permissions + await [Permission.microphone, Permission.camera].request(); + + // Create an instance of the Agora engine + agoraEngine = createAgoraRtcEngine(); + await agoraEngine!.initialize(RtcEngineContext( + appId: appId, + logConfig: + const LogConfig(fileSizeInKB: 2048, level: LogLevel.logLevelWarn))); + + if (currentProduct != ProductName.voiceCalling) { + await agoraEngine!.enableVideo(); + } + + // Enable the dual stream mode + agoraEngine!.enableDualStreamMode(enabled: true); + + // Set audio profile and audio scenario. + agoraEngine!.setAudioProfile( + profile: AudioProfileType.audioProfileDefault, + scenario: AudioScenarioType.audioScenarioChatroom); + + // Set the video configuration + VideoEncoderConfiguration videoConfig = const VideoEncoderConfiguration( + mirrorMode: VideoMirrorModeType.videoMirrorModeAuto, + frameRate: 10, + bitrate: standardBitrate, + dimensions: VideoDimensions(width: 640, height: 360), + orientationMode: OrientationMode.orientationModeAdaptive, + degradationPreference: DegradationPreference.maintainBalanced + ); + + // Apply the video configuration + agoraEngine!.setVideoEncoderConfiguration(videoConfig); + + // Start the probe test + startProbeTest(); + + // Register the event handler + agoraEngine!.registerEventHandler(getEventHandler()); + } + ``` + + + ```swift + func setupEngine() -> AgoraRtcEngineKit { + let engine = super.setupEngine() + + // Set Audio Scenario + engine.setAudioScenario(.gameStreaming) + + // Enable dual stream mode + engine.setDualStreamMode(.enableSimulcastStream) + engine.setAudioProfile(.default) + + // Set the video configuration + let videoConfig = AgoraVideoEncoderConfiguration( + size: CGSize(width: 640, height: 360), + frameRate: .fps10, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative, + mirrorMode: .auto + ) + engine.setVideoEncoderConfiguration(videoConfig) + + return engine + } + ``` + + + - setAudioScenario(_:) + - setDualStreamMode(_:) + - setAudioProfile(_:) + - AgoraVideoEncoderConfiguration + - setVideoEncoderConfiguration(_:) + + + - setAudioScenario(_:) + - setDualStreamMode(_:) + - setAudioProfile(_:) + - AgoraVideoEncoderConfiguration + - setVideoEncoderConfiguration(_:) + + + + + + ``` csharp + public override void SetupAgoraEngine() + { + base.SetupAgoraEngine(); + + // Specify a path for the log file. + agoraEngine.SetLogFile("/path/to/folder/agorasdk1.log"); + + // Set the log file size. + agoraEngine.SetLogFileSize(256); // Range 128-20480 Kb + + // Specify a log level. + agoraEngine.SetLogLevel(LOG_LEVEL.LOG_LEVEL_WARN); + + // Enable the dual stream mode. + agoraEngine.EnableDualStreamMode(true); + + // Set audio profile and audio scenario. + agoraEngine.SetAudioProfile(AUDIO_PROFILE_TYPE.AUDIO_PROFILE_DEFAULT, AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_CHATROOM); + + // Set the video profile. + VideoEncoderConfiguration videoConfig = new VideoEncoderConfiguration(); + + // Set mirror mode. + videoConfig.mirrorMode = VIDEO_MIRROR_MODE_TYPE.VIDEO_MIRROR_MODE_DISABLED; + + // Set frame rate. + videoConfig.frameRate = (int)FRAME_RATE.FRAME_RATE_FPS_15; + + // Set bitrate. + videoConfig.bitrate = (int)BITRATE.STANDARD_BITRATE; + + // Set dimensions. + videoConfig.dimensions = new VideoDimensions(640, 360); + + // Set orientation mode. + videoConfig.orientationMode = ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; + + // Set degradation preference. + videoConfig.degradationPreference = DEGRADATION_PREFERENCE.MAINTAIN_BALANCED; + + // Set the latency level + videoConfig.advanceOptions.compressionPreference = COMPRESSION_PREFERENCE.PREFER_LOW_LATENCY; + + // Apply the configuration. + agoraEngine.SetVideoEncoderConfiguration(videoConfig); + + // Attach the event handler + agoraEngine.InitEventHandler(new CallQualityEventHandler(this)); + } + ``` + + + ```csharp + public override void SetupAgoraEngine() + { + base.SetupAgoraEngine(); + + // Specify a path for the log file. + agoraEngine.SetLogFile("/path/to/folder/agorasdk1.log"); + + // Set the log file size. + agoraEngine.SetLogFileSize(256); // Range 128-20480 Kb + + // Specify a log level. + agoraEngine.SetLogLevel(LOG_LEVEL.LOG_LEVEL_WARN); + + // Enable the dual stream mode. + agoraEngine.EnableDualStreamMode(true); + + // Set audio profile and audio scenario. + agoraEngine.SetAudioProfile(AUDIO_PROFILE_TYPE.AUDIO_PROFILE_DEFAULT, AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_CHATROOM); + + // Set the video profile. + VideoEncoderConfiguration videoConfig = new VideoEncoderConfiguration(); + + // Set mirror mode. + videoConfig.mirrorMode = VIDEO_MIRROR_MODE_TYPE.VIDEO_MIRROR_MODE_DISABLED; + + // Set framerate. + videoConfig.frameRate = (int)FRAME_RATE.FRAME_RATE_FPS_15; + + // Set bitrate. + videoConfig.bitrate = (int)BITRATE.STANDARD_BITRATE; + + // Set dimensions. + videoConfig.dimensions = new VideoDimensions(640, 360); + + // Set orientation mode. + videoConfig.orientationMode = ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; + + // Set degradation preference. + videoConfig.degradationPreference = DEGRADATION_PREFERENCE.MAINTAIN_BALANCED; + + // Set the latency level + videoConfig.advanceOptions.compressionPreference = COMPRESSION_PREFERENCE.PREFER_LOW_LATENCY; + + // Apply the configuration. + agoraEngine.SetVideoEncoderConfiguration(videoConfig); + + // Attach the eventHandler + agoraEngine.InitEventHandler(new CallQualityEventHandler(this)); + } + ``` + + - SetLogFile + - SetLogFileSize + - SetLogLevel + - EnableDualStreamMode + - SetAudioProfile + - SetVideoEncoderConfiguration + - InitEventHandler + + + ```csharp + public override void SetupAgoraEngine() + { + base.SetupAgoraEngine(); + + // Specify a path for the log file. + agoraEngine.SetLogFile("/path/to/folder/agorasdk1.log"); + + // Set the log file size. + agoraEngine.SetLogFileSize(256); // Range 128-20480 Kb + + // Specify a log level. + agoraEngine.SetLogLevel(LOG_LEVEL.LOG_LEVEL_WARN); + + // Set audio profile and audio scenario. + agoraEngine.SetAudioProfile(AUDIO_PROFILE_TYPE.AUDIO_PROFILE_DEFAULT, AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_CHATROOM); + + // Attach the eventHandler + agoraEngine.InitEventHandler(new CallQualityEventHandler(this)); + } + ``` + - SetLogFile + - SetLogFileSize + - SetLogLevel + - InitEventHandler + + + + + ```typescript + const agoraEngine = useRTCClient(AgoraRTC.createClient({ codec: "vp8", mode: config.selectedProduct })); + + + + + + + const callQualityEssentials = async () => { + try { + AgoraRTC.setLogLevel(2); // Info level + await agoraEngine.enableDualStream(); + } catch (error) { + console.log(error); + } + await localCameraTrack?.setEncoderConfiguration({ + width: 640, + height: { ideal: 480, min: 400, max: 500 }, + frameRate: 15, + bitrateMin: 600, + bitrateMax: 1000, + }); + }; + ``` + - useRTCClient + - AgoraRTCProvider + - setEncoderConfiguration + diff --git a/assets/code/video-sdk/ensure-channel-quality/show-stats.mdx b/assets/code/video-sdk/ensure-channel-quality/show-stats.mdx new file mode 100644 index 000000000..819b53d67 --- /dev/null +++ b/assets/code/video-sdk/ensure-channel-quality/show-stats.mdx @@ -0,0 +1,45 @@ + + ```typescript + const showStatistics = () => { + const localAudioStats = agoraEngine.getLocalAudioStats(); + console.log("Local audio stats:", localAudioStats); + + const localVideoStats = agoraEngine.getLocalVideoStats(); + console.log("Local video stats:", localVideoStats); + + const rtcStats = agoraEngine.getRTCStats(); + console.log("Channel statistics:", rtcStats); + }; + ``` + - getLocalAudioStats + - getLocalVideoStats + - getRTCStats + + + +```js + const getStatistics = async (remoteUid) => { + const localAudioStats = agoraEngine.getLocalAudioStats(); + const localVideoStats = agoraEngine.getLocalVideoStats(); + let remoteAudioStats; + let remoteVideoStats; + if (remoteUid !== undefined) { + remoteAudioStats = agoraEngine.getRemoteAudioStats()[remoteUid]; + remoteVideoStats = agoraEngine.getRemoteVideoStats()[remoteUid]; + } + const rtcStats = agoraEngine.getRTCStats(); + return { + localAudioStats, + localVideoStats, + remoteAudioStats, + remoteVideoStats, + rtcStats, + }; + }; +``` +- getLocalAudioStats +- getLocalVideoStats +- getRemoteAudioStats +- getRemoteVideoStats +- getRTCStats + diff --git a/assets/code/video-sdk/ensure-channel-quality/swift/implement-call-quality-view.mdx b/assets/code/video-sdk/ensure-channel-quality/swift/implement-call-quality-view.mdx deleted file mode 100644 index 65413fc68..000000000 --- a/assets/code/video-sdk/ensure-channel-quality/swift/implement-call-quality-view.mdx +++ /dev/null @@ -1,25 +0,0 @@ - - ``` swift - public override func setupEngine() -> AgoraRtcEngineKit { - let engine = super.setupEngine() - - // Set Audio Scenario - engine.setAudioScenario(.gameStreaming) - - // Enable dual stream mode - engine.setDualStreamMode(.enableSimulcastStream) - engine.setAudioProfile(.default) - - // Set the video configuration - let videoConfig = AgoraVideoEncoderConfiguration( - size: CGSize(width: 640, height: 360), - frameRate: .fps10, - bitrate: AgoraVideoBitrateStandard, - orientationMode: .adaptative, - mirrorMode: .auto - ) - engine.setVideoEncoderConfiguration(videoConfig) - - return engine - } - ``` diff --git a/assets/code/video-sdk/ensure-channel-quality/swift/implement-declarations.mdx b/assets/code/video-sdk/ensure-channel-quality/swift/implement-declarations.mdx deleted file mode 100644 index 4e0e95854..000000000 --- a/assets/code/video-sdk/ensure-channel-quality/swift/implement-declarations.mdx +++ /dev/null @@ -1,13 +0,0 @@ - -```swift -var networkStatusLabel: NSTextField! -var networkStatus: NSTextField! -var qualityButton: NSButton! -``` - - -```swift -var networkStatusLabel: UILabel! -var networkStatus: UILabel! -``` - \ No newline at end of file diff --git a/assets/code/video-sdk/ensure-channel-quality/switch-quality.mdx b/assets/code/video-sdk/ensure-channel-quality/switch-quality.mdx new file mode 100644 index 000000000..0d2e23169 --- /dev/null +++ b/assets/code/video-sdk/ensure-channel-quality/switch-quality.mdx @@ -0,0 +1,127 @@ + + + ```kotlin + fun setStreamQuality(remoteUid: Int, highQuality: Boolean) { + // Set the stream type of the remote video + if (highQuality) { + agoraEngine!!.setRemoteVideoStreamType(remoteUid, Constants.VIDEO_STREAM_HIGH) + } else { + agoraEngine!!.setRemoteVideoStreamType(remoteUid, Constants.VIDEO_STREAM_LOW) + } + } + ``` + - setRemoteVideoStreamType + + To automatically switch to lower-quality video or audio-only mode when network conditions are poor, call `setRemoteSubscribeFallbackOption` to set the desired fallback. The SDK triggers the `onRemoteSubscribeFallbackToAudioOnly` callback when switching back and forth to audio-only mode due to network conditions. + + + + ```dart + void setVideoQuality(int remoteUid, bool isHighQuality) { + if (isHighQuality) { + agoraEngine!.setRemoteVideoStreamType(uid: remoteUid, + streamType: VideoStreamType.videoStreamHigh); + } else { + agoraEngine!.setRemoteVideoStreamType(uid: remoteUid, + streamType: VideoStreamType.videoStreamLow); + } + } + ``` + + + ```swift + func setStreamQuality(for uid: UInt, to quality: AgoraVideoStreamType) { + agoraEngine.setRemoteVideoStream(uid, type: quality) + } + ``` + + - AgoraVideoStreamType + - setRemoteVideoStream(_:type:) + + To automatically switch to lower-quality video or audio-only mode when network conditions are poor, call `setRemoteSubscribeFallbackOption(_:)` to set the desired fallback. The SDK triggers the `rtcEngine(_:didRemoteSubscribeFallbackToAudioOnly:byUid:)` callback when switching back and forth to audio-only mode due to network conditions. + + + + - AgoraVideoStreamType + - setRemoteVideoStream(_:type:) + + To automatically switch to lower-quality video or audio-only mode when network conditions are poor, call `setRemoteSubscribeFallbackOption(_:)` to set the desired fallback. The SDK triggers the `rtcEngine(_:didRemoteSubscribeFallbackToAudioOnly:byUid:)` callback when switching back and forth to audio-only mode due to network conditions. + + + + + ```csharp + // Switch between high and low remote user video quality. + public void SetLowStreamQuality() + { + if(remoteUid > 1) + { + agoraEngine.SetRemoteVideoStreamType(remoteUid, VIDEO_STREAM_TYPE.VIDEO_STREAM_LOW); + Debug.Log("Switching to low-quality video"); + } + else + { + Debug.Log("No remote user in the channel"); + } + } + public void SetHighStreamQuality() + { + if (remoteUid > 1) + { + agoraEngine.SetRemoteVideoStreamType(remoteUid, VIDEO_STREAM_TYPE.VIDEO_STREAM_HIGH); + Debug.Log("Switching to high-quality video"); + } + else + { + Debug.Log("No remote user in the channel"); + } + } + ``` + - SetRemoteVideoStreamType + + + ```javascript + const handleVSDKEvents = (eventName, ...args) => { + switch (eventName) { + case "user-published": + if (args[1] == "video") { + // ... + // Change stream quality on click + document.getElementById(remotePlayerContainer.id).addEventListener("click", function () { + if (isHighRemoteVideoQuality === false) { + agoraManager.setRemoteVideoStreamType(channelParameters.remoteUid, 0); + isHighRemoteVideoQuality = true; + } else { + agoraManager.setRemoteVideoStreamType(channelParameters.remoteUid, 1); + isHighRemoteVideoQuality = false; + } + }); + } + } + }; + // Play the remote video track. + channelParameters.remoteVideoTrack.play(remotePlayerContainer); + ``` + + - setRemoteVideoStreamType + + + + ```typescript + const setRemoteVideoQuality = () => { + if (!remoteUser) { + console.log("No remote user in the channel"); + return; + } + + const newQualityState = !isHighRemoteVideoQuality; + const streamType = newQualityState ? 0 : 1; + + agoraEngine + .setRemoteVideoStreamType(remoteUser.uid, streamType) + .then(() => setVideoQualityState(newQualityState)) + .catch((error) => console.error(error)); + }; + ``` + + diff --git a/assets/code/video-sdk/ensure-channel-quality/test-hardware.mdx b/assets/code/video-sdk/ensure-channel-quality/test-hardware.mdx new file mode 100644 index 000000000..e9703b6a0 --- /dev/null +++ b/assets/code/video-sdk/ensure-channel-quality/test-hardware.mdx @@ -0,0 +1,407 @@ + + + ```kotlin + fun startEchoTest(): SurfaceView { + if (agoraEngine == null) setupAgoraEngine() + // Set test configuration parameters + val echoConfig = EchoTestConfiguration() + echoConfig.enableAudio = true + echoConfig.enableVideo = true + echoConfig.channelId = channelName + echoConfig.intervalInSeconds = 2 // Interval between recording and playback + // Set up a SurfaceView + val localSurfaceView = SurfaceView(mContext) + localSurfaceView.visibility = View.VISIBLE + // Call setupLocalVideo with a VideoCanvas having uid set to 0. + agoraEngine!!.setupLocalVideo( + VideoCanvas( + localSurfaceView, + VideoCanvas.RENDER_MODE_HIDDEN, + 0 + ) + ) + echoConfig.view = localSurfaceView + + // Get a token from the server or from the config file + if (serverUrl.contains("http")) { // A valid server url is available + // Fetch a token from the server for channelName + fetchToken(channelName, 0, object : TokenCallback { + override fun onTokenReceived(rtcToken: String?) { + // Set the token in the config + echoConfig.token = rtcToken + // Start the echo test + agoraEngine!!.startEchoTest(echoConfig) + } + + override fun onError(errorMessage: String) { + // Handle the error + sendMessage("Error: $errorMessage") + } + }) + } else { // use the token from the config.json file + echoConfig.token = config!!.optString("rtcToken") + // Start the echo test + agoraEngine!!.startEchoTest(echoConfig) + } + return localSurfaceView + } + + fun stopEchoTest() { + agoraEngine!!.stopEchoTest() + destroyAgoraEngine() + } + ``` + + - EchoTestConfiguration + - startEchoTest + - stopEchoTest + - setupLocalVideo + + + + ```kotlin + fun startEchoTest() { + if (agoraEngine == null) setupAgoraEngine() + // Set test configuration parameters + val echoConfig = EchoTestConfiguration() + echoConfig.enableAudio = true + echoConfig.channelId = channelName + echoConfig.intervalInSeconds = 2 // Interval between recording and playback + + // Get a token from the server or from the config file + if (serverUrl.contains("http")) { // A valid server url is available + // Fetch a token from the server for channelName + fetchToken(channelName, 0, object : TokenCallback { + override fun onTokenReceived(rtcToken: String?) { + // Set the token in the config + echoConfig.token = rtcToken + // Start the echo test + agoraEngine!!.startEchoTest(echoConfig) + } + + override fun onError(errorMessage: String) { + // Handle the error + sendMessage("Error: $errorMessage") + } + }) + } else { // use the token from the config.json file + echoConfig.token = config!!.optString("rtcToken") + // Start the echo test + agoraEngine!!.startEchoTest(echoConfig) + } + } + + fun stopEchoTest() { + agoraEngine!!.stopEchoTest() + destroyAgoraEngine() + } + ``` + + - EchoTestConfiguration + - startEchoTest + - stopEchoTest + + + + + ```dart + void startEchoTest() async { + if (agoraEngine == null) setupAgoraEngine(); + + // Get a token for the test + String token; + if (config['serverUrl'].toString().contains('http')){ + // Use the uid 0xFFFFFFFF to get a token for the echo test + // Ensure that the channel name is unique for each user when running the echo test + token = await fetchToken(0xFFFFFFFF, channelName); + } else { + token = config['rtcToken']; + } + + // Set test configuration parameters + EchoTestConfiguration echoConfig = EchoTestConfiguration( + enableAudio: true, + enableVideo: false, + channelId: channelName, + intervalInSeconds: 2, // Interval between recording and playback + token: token, + ); + + // Start the echo test + agoraEngine!.startEchoTest(echoConfig); + } + + void stopEchoTest() { + agoraEngine!.stopEchoTest(); + localUid = config['uid']; + destroyAgoraEngine(); + } + ``` + + + ```swift + func startEchoTest(channel: String) async throws -> Int32 { + let echoConfig = AgoraEchoTestConfiguration() + echoConfig.enableAudio = true + echoConfig.enableVideo = true + echoConfig.channelId = channel + echoConfig.intervalInSeconds = 2 // Interval between recording and playback + + echoConfig.view = echoView + echoConfig.token = <#Token#> + let localCanvas = AgoraRtcVideoCanvas() + localCanvas.view = echoConfig.view + localCanvas.uid = 0 + + agoraEngine.setupLocalVideo(localCanvas) + + return agoraEngine.startEchoTest(withConfig: echoConfig) + } + + func stopEchoTest() -> Int32 { + self.agoraEngine.stopPreview() + self.agoraEngine.enableLocalVideo(false) + return agoraEngine.stopEchoTest() + } + ``` + + + - AgoraEchoTestConfiguration + - AgoraRtcVideoCanvas + - setupLocalVideo(_:) + - startEchoTest(withConfig:) + + + - AgoraEchoTestConfiguration + - AgoraRtcVideoCanvas + - setupLocalVideo(_:) + - startEchoTest(withConfig:) + + + + + + ```csharp + // Get the list of available audio devices. + private void GetAudioRecordingDevice() + { + _audioDeviceManager = agoraEngine.GetAudioDeviceManager(); + _audioRecordingDeviceInfos = _audioDeviceManager.EnumerateRecordingDevices(); + audioDevices = new List(); + + for (var i = 0; i < _audioRecordingDeviceInfos.Length; i++) + { + Debug.Log(string.Format("AudioRecordingDevice device index: {0}, name: {1}, id: {2}", i, + _audioRecordingDeviceInfos[i].deviceName, _audioRecordingDeviceInfos[i].deviceId)); + audioDevices.Add(_audioRecordingDeviceInfos[i].deviceName); + } + } + + // Get the list of available video devices. + private void GetVideoDeviceManager() + { + _videoDeviceManager = agoraEngine.GetVideoDeviceManager(); + _videoDeviceInfos = _videoDeviceManager.EnumerateVideoDevices(); + + videoDevices = new List(); + for (var i = 0; i < _videoDeviceInfos.Length; i++) + { + Debug.Log(string.Format("VideoDeviceManager device index: {0}, name: {1}, id: {2}", i, + _videoDeviceInfos[i].deviceName, _videoDeviceInfos[i].deviceId)); + videoDevices.Add(_videoDeviceInfos[i].deviceName); + } + } + + // Device test to check if the audio and video device is working properly. Only valid before joining the channel. + public void StartAudioVideoDeviceTest(string selectedAudioDevice, string selectedVideoDevice) + { + Debug.Log("Please conduct the device test before joining the channel."); + SetupAgoraEngine(); + foreach (var device in _audioRecordingDeviceInfos) + { + if(selectedAudioDevice == device.deviceName) + { + _audioDeviceManager.SetRecordingDevice(device.deviceId); + } + } + _audioDeviceManager.StartAudioDeviceLoopbackTest(500); + foreach (var device in _videoDeviceInfos) + { + if(selectedVideoDevice == device.deviceName) + { + _videoDeviceManager.SetDevice(device.deviceId); + } + } + hWnd = CreateWindowEx( + 0, + "Static", + "My Window", + WS_OVERLAPPEDWINDOW | WS_VISIBLE, + 100, + 100, + 640, + 480, + IntPtr.Zero, + IntPtr.Zero, + Marshal.GetHINSTANCE(typeof(EnsureCallQuality).Module), + IntPtr.Zero); + ShowWindow(hWnd, SW_SHOW); + _videoDeviceManager.StartDeviceTest(hWnd); + } + public void StopAudioVideoDeviceTest() + { + DestroyWindow(hWnd); + _audioDeviceManager.StopAudioDeviceLoopbackTest(); + _videoDeviceManager.StopDeviceTest(); + DestroyEngine(); + } + ``` + - SetRecordingDevice + - StartAudioDeviceLoopbackTest + - SetDevice + - StopAudioDeviceLoopbackTest + - GetAudioDeviceManager + - GetVideoDeviceManager + - IVideoDeviceManager + - EnumerateVideoDevices + - IAudioDeviceManager + - EnumerateRecordingDevices + + + ```csharp + // Get the list of available audio devices. + private void GetAudioRecordingDevice() + { + _audioDeviceManager = agoraEngine.GetAudioDeviceManager(); + _audioRecordingDeviceInfos = _audioDeviceManager.EnumerateRecordingDevices(); + audioDevices = new List(); + + for (var i = 0; i < _audioRecordingDeviceInfos.Length; i++) + { + Debug.Log(string.Format("AudioRecordingDevice device index: {0}, name: {1}, id: {2}", i, + _audioRecordingDeviceInfos[i].deviceName, _audioRecordingDeviceInfos[i].deviceId)); + audioDevices.Add(_audioRecordingDeviceInfos[i].deviceName); + } + } + // Device test to check if the audio device is working properly. Only valid before joining the channel. + public void StartDeviceTest(string selectedAudioDevice) + { + Debug.Log("Please conduct the device test before joining the channel."); + SetupAgoraEngine(); + foreach (var device in _audioRecordingDeviceInfos) + { + if(selectedAudioDevice == device.deviceName) + { + _audioDeviceManager.SetRecordingDevice(device.deviceId); + } + } + _audioDeviceManager.StartAudioDeviceLoopbackTest(500); + } + public void StopDeviceTest() + { + _audioDeviceManager.StopAudioDeviceLoopbackTest(); + DestroyEngine(); + } + ``` + - SetRecordingDevice + - StartAudioDeviceLoopbackTest + - StopAudioDeviceLoopbackTest + - GetAudioDeviceManager + - IAudioDeviceManager + - EnumerateRecordingDevices + + + + + ```javascript + const getDevices = async () => { + const devices = await AgoraRTC.getDevices(); + const audioDevices = devices.filter(function (device) { + return device.kind === "audioinput"; + }); + return { + audioDevices, + }; + }; + + const createTestTracks = async (camera, mic) => { + const audioTrack = AgoraRTC.createMicrophoneAudioTrack({ + microphoneId: mic, + }); + return { + audioTrack, + }; + }; + ``` + - getDevices + - createMicrophoneAudioTrack + + + ```javascript + const getDevices = async () => { + const devices = await AgoraRTC.getDevices(); + const audioDevices = devices.filter(function (device) { + return device.kind === "audioinput"; + }); + const videoDevices = devices.filter(function (device) { + return device.kind === "videoinput"; + }); + return { + audioDevices, + videoDevices, + }; + }; + + const createTestTracks = async (camera, mic) => { + const videoTrack = AgoraRTC.createCameraVideoTrack({ + cameraId: camera, + }); + const audioTrack = AgoraRTC.createMicrophoneAudioTrack({ + microphoneId: mic, + }); + return { + videoTrack, + audioTrack, + }; + }; + ``` + - getDevices + - createMicrophoneAudioTrack + - createCameraVideoTrack + + + + ```typescript + const handleStartDeviceTest = () => { + setDeviceTestState(true); + }; + + const handleStopDeviceTest = () => { + setDeviceTestState(false); + }; + const VideoDeviceTestComponent: React.FC<{ localCameraTrack: ICameraVideoTrack | null }> = ({ localCameraTrack }) => { + useJoin({ appid: config.appId, channel: config.channelName, token: config.rtcToken }, true); + + return ( +
+ +
+ ); + }; + const AudioDeviceTestComponent: React.FC<{ localMicrophoneTrack: ILocalAudioTrack }> = ({ localMicrophoneTrack }) => { + useAutoPlayAudioTrack(localMicrophoneTrack, true); + const volume = useVolumeLevel(localMicrophoneTrack); + + return ( +
+

local Audio Volume: {Math.floor(volume * 100)}

+
+ ); + }; + ``` + - useVolumeLevel + - useJoin + - useAutoPlayAudioTrack + - LocalVideoTrack + +
diff --git a/assets/code/video-sdk/geofencing/combine-geofence.mdx b/assets/code/video-sdk/geofencing/combine-geofence.mdx new file mode 100644 index 000000000..9004d931e --- /dev/null +++ b/assets/code/video-sdk/geofencing/combine-geofence.mdx @@ -0,0 +1,34 @@ + + ```kotlin + // Area codes support bitwise operations + config.mAreaCode = AREA_CODE_NA or AREA_CODE_EU + ``` + + ```kotlin + // Exclude Mainland China from the regions for connection + config.mAreaCode = AREA_CODE_GLOB ^ AREA_CODE_CN + ``` + - List of AreaCodes + + + ```swift + // Your app will only connect to Agora SD-RTN located in Europe and India. + config.areaCode = AgoraAreaCodeType( + rawValue: AgoraAreaCodeType.EUR.rawValue | AgoraAreaCodeType.IN.rawValue + ) + ``` + + + - AgoraAreaCodeType + + + - AgoraAreaCodeType + + + + ```ts + AgoraRTC.setArea({ + areaCode: [AREAS.NORTH_AMERICA, AREAS.ASIA] + }) + ``` + diff --git a/assets/code/video-sdk/geofencing/set-geofence.mdx b/assets/code/video-sdk/geofencing/set-geofence.mdx new file mode 100644 index 000000000..91acfb2c9 --- /dev/null +++ b/assets/code/video-sdk/geofencing/set-geofence.mdx @@ -0,0 +1,94 @@ + + ```kotlin + // Set the engine configuration + val config = RtcEngineConfig() + config.mContext = mContext + config.mAppId = appId + config.mEventHandler = iRtcEngineEventHandler + + // Set the geofencing area code(s) + config.mAreaCode = AREA_CODE_NA + + // Create an RtcEngine instance + agoraEngine = RtcEngine.create(config) + ``` + - RtcEngineConfig + - RtcEngine.create + + + ```swift + let config = AgoraRtcEngineConfig() + // Your app will only connect to Agora SD-RTN located in North America. + config.areaCode = .NA; + + let eng = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + // ... + ``` + + + - AgoraRtcEngineConfig + - AgoraRtcEngineConfig/areaCode + - AgoraRtcEngineKit/sharedEngine(with:delegate:) + + + - AgoraRtcEngineConfig + - AgoraRtcEngineConfig/areaCode + - AgoraRtcEngineKit/sharedEngine(with:delegate:) + + + + ```csharp + public override void SetupAgoraEngine() + { + // Set the region of your choice. + region = AREA_CODE.AREA_CODE_CN; + + base.SetupAgoraEngine(); + } + ``` + + - AREA_CODE + - RtcEngineContext + - Initialize + + + - AREA_CODE + - RtcEngineContext + - Initialize + + + + + ```typescript + const useGeofencing = () => { + useEffect(() => { + AgoraRTC.setArea({ + areaCode: [AREAS.NORTH_AMERICA, AREAS.ASIA] + }) + }, []); + }; + + function EnableGeofencing() { + useGeofencing(); + + return ( +
+ +
+ ); + } + ``` + - setArea + +
+ + ```js + // Your app will only connect to Agora SD-RTN located in North America. + AgoraRTC.setArea({ + areaCode:"ASIA" + }) + // You can use [] to include more than one region. + ``` + - setArea + - AREAS + diff --git a/assets/code/video-sdk/get-started-sdk/create-engine.mdx b/assets/code/video-sdk/get-started-sdk/create-engine.mdx new file mode 100644 index 000000000..3d6a352da --- /dev/null +++ b/assets/code/video-sdk/get-started-sdk/create-engine.mdx @@ -0,0 +1,286 @@ + + + ```kotlin + protected open fun setupAgoraEngine(): Boolean { + try { + // Set the engine configuration + val config = RtcEngineConfig() + config.mContext = mContext + config.mAppId = appId + // Assign an event handler to receive engine callbacks + config.mEventHandler = iRtcEngineEventHandler + // Create an RtcEngine instance + agoraEngine = RtcEngine.create(config) + // By default, the video module is disabled, call enableVideo to enable it. + agoraEngine!!.enableVideo() + } catch (e: Exception) { + sendMessage(e.toString()) + return false + } + return true + } + ``` + + - RtcEngine + - RtcEngineConfig + - create + - enableVideo + + + ```kotlin + protected open fun setupAgoraEngine(): Boolean { + try { + // Set the engine configuration + val config = RtcEngineConfig() + config.mContext = mContext + config.mAppId = appId + // Assign an event handler to receive engine callbacks + config.mEventHandler = iRtcEngineEventHandler + // Create an RtcEngine instance + agoraEngine = RtcEngine.create(config) + } catch (e: Exception) { + sendMessage(e.toString()) + return false + } + return true + } + ``` + + - RtcEngine + - RtcEngineConfig + - create + + + + + ```dart + Future setupAgoraEngine() async { + // Retrieve or request camera and microphone permissions + await [Permission.microphone, Permission.camera].request(); + + // Create an instance of the Agora engine + agoraEngine = createAgoraRtcEngine(); + await agoraEngine!.initialize(RtcEngineContext(appId: appId)); + + if (currentProduct != ProductName.voiceCalling) { + await agoraEngine!.enableVideo(); + } + + // Register the event handler + agoraEngine!.registerEventHandler(getEventHandler()); + } + ``` + + + ```swift + // The Agora RTC Engine Kit for the session. + public var agoraEngine: AgoraRtcEngineKit { + if let engine { return engine } + let engine = setupEngine() + self.engine = engine + return engine + } + + open func setupEngine() -> AgoraRtcEngineKit { + let eng = AgoraRtcEngineKit.sharedEngine(withAppId: appId, delegate: self) + if DocsAppConfig.shared.product != .voice { + eng.enableVideo() + } else { eng.enableAudio() } + eng.setClientRole(role) + return eng + } + ``` + + + - sharedEngine(withAppId:delegate:) + - enableVideo() + - enableAudio() + - setClientRole(_:) + + + - sharedEngine(withAppId:delegate:) + - enableVideo() + - enableAudio() + - setClientRole(_:) + + + + + ```csharp + // Define a public function called SetupAgoraEngine to setup the video SDK engine. + public virtual void SetupAgoraEngine() + { + if(_appID == "" || _token == "") + { + Debug.Log("Please set an app ID and a token in the config file."); + return; + } + // Create an instance of the video SDK engine. + agoraEngine = Agora.Rtc.RtcEngine.CreateAgoraRtcEngine(); + + RtcEngineContext context = new RtcEngineContext(_appID, 0, CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_COMMUNICATION, + AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT, region, null); + + agoraEngine.Initialize(context); + + // Enable the audio module. + agoraEngine.EnableAudio(); + + // Set the user role as broadcaster. + agoraEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER); + + // Attach the eventHandler + InitEventHandler(); + + } + ``` + - CreateAgoraRtcEngine + + - Initialize + + - EnableAudio + + - SetClientRole + + + + ```csharp + // Define a public function called SetupAgoraEngine to setup the video SDK engine. + public virtual void SetupAgoraEngine() + { + if(_appID == "" || _token == "") + { + Debug.Log("Please set an app ID and a token in the config file."); + return; + } + // Create an instance of the video SDK engine. + agoraEngine = Agora.Rtc.RtcEngine.CreateAgoraRtcEngine(); + + // Set context configuration based on the product type + CHANNEL_PROFILE_TYPE channelProfile = configData.product == "Video Calling" + ? CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_COMMUNICATION + : CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING; + + RtcEngineContext context = new RtcEngineContext(_appID, 0, channelProfile, + AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT, region, null); + + agoraEngine.Initialize(context); + + // Enable the video module. + agoraEngine.EnableVideo(); + + // Set the user role as broadcaster. + agoraEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER); + + // Attach the eventHandler + InitEventHandler(); + + } + ``` + - CreateAgoraRtcEngine + + - Initialize + + - EnableVideo + + - SetClientRole + + + + + ```javascript + const AgoraRTCManager = async (eventsCallback) => { + let agoraEngine = null; + + // Set up the signaling engine with the provided App ID, UID, and configuration + const setupAgoraEngine = async () => { + agoraEngine = new AgoraRTC.createClient({ mode: "live", codec: "vp9" }); + }; + + await setupAgoraEngine(); + + const getAgoraEngine = () => { + return agoraEngine; + }; + }; + ``` + + + ```javascript + const AgoraRTCManager = async (eventsCallback) => { + let agoraEngine = null; + + // Set up the signaling engine with the provided App ID, UID, and configuration + const setupAgoraEngine = async () => { + agoraEngine = new AgoraRTC.createClient({ mode: "rtc", codec: "vp9" }); + }; + + await setupAgoraEngine(); + + const getAgoraEngine = () => { + return agoraEngine; + }; + }; + ``` + + - AgoraRTC.createClient + - IAgoraRTCRemoteUser + + + ```typescript + const agoraEngine = useRTCClient(AgoraRTC.createClient({ codec: "vp8", mode: config.selectedProduct })); + + }/> + + ``` + - useRTCClient + - AgoraRTCProvider + + + + + + ```cpp + void AgoraManager::setupVideoSDKEngine() + { + // Check if the engine initialized successfully. + agoraEngine = createAgoraRtcEngine(); + if (!agoraEngine) + { + return; + } + AgoraEventStrategy->SetMsgReceiver(gui->getGuiWindowReference()); + + // Create an instance of RtcEngineContext to initialize the engine. + RtcEngineContext context; + // Pass your app ID to the context. + context.appId = appId.c_str(); + // Pass an object of agoraEventHandler class to receive callbacks. + context.eventHandler = AgoraEventStrategy.get(); + // Set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + context.channelProfile = CHANNEL_PROFILE_LIVE_BROADCASTING; + // Initialize the engine using the context variable. + agoraEngine->initialize(context); + // Set the user role to Host. + agoraEngine->setClientRole(CLIENT_ROLE_TYPE::CLIENT_ROLE_BROADCASTER); + + // Enable the microphone to create the local audio stream. + agoraEngine->enableAudio(); + // Enable the local video capturer. + agoraEngine->enableVideo(); + } + ``` + - createAgoraRtcEngine + + - initialize + + - setClientRole + + - enableVideo + + - enableAudio + + + + + diff --git a/assets/code/video-sdk/get-started-sdk/declare-variables.mdx b/assets/code/video-sdk/get-started-sdk/declare-variables.mdx new file mode 100644 index 000000000..99715bb57 --- /dev/null +++ b/assets/code/video-sdk/get-started-sdk/declare-variables.mdx @@ -0,0 +1,140 @@ + + ```kotlin + protected var agoraEngine: RtcEngine? = null // The RTCEngine instance + protected var mListener: AgoraManagerListener? = null // The event handler to notify the UI of agoraEngine events + protected val appId: String // Your App ID from Agora console + var channelName: String // The name of the channel to join + var localUid: Int // UID of the local user + var remoteUids = HashSet() // An object to store uids of remote users + var isJoined = false // Status of the video call + private set + var isBroadcaster = true // Local user role + ``` + + + ```dart + late Map config; // Configuration parameters + int localUid = -1; + String appId = "", channelName = ""; + List remoteUids = []; // Uids of remote users in the channel + bool isJoined = false; // Indicates if the local user has joined the channel + bool isBroadcaster = true; // Client role + RtcEngine? agoraEngine; // Agora engine instance + ``` + + + ```swift + // The Agora App ID for the session. + public let appId: String + // The client's role in the session. + public var role: AgoraClientRole = .audience { + didSet { agoraEngine.setClientRole(role) } + } + + // The set of all users in the channel. + @Published public var allUsers: Set = [] + + // Integer ID of the local user. + @Published public var localUserId: UInt = 0 + + private var engine: AgoraRtcEngineKit? + ``` + + + + ```csharp + // Define some variables to be used later. + internal string _appID; + internal string _channelName; + internal string _token; + internal uint remoteUid; + internal IRtcEngine agoraEngine; + internal VideoSurface LocalView; + internal VideoSurface RemoteView; + internal ConfigData configData; + internal AREA_CODE region = AREA_CODE.AREA_CODE_GLOB; + internal string userRole = ""; + + #if (UNITY_2018_3_OR_NEWER && UNITY_ANDROID) + // Define an ArrayList of permissions required for Android devices. + private ArrayList permissionList = new ArrayList() { Permission.Camera, Permission.Microphone }; + #endif + ``` + + + ```csharp + // Define some variables to be used later. + internal string _appID; + internal string _channelName; + internal string _token; + internal uint remoteUid; + internal IRtcEngine agoraEngine; + internal ConfigData configData; + internal AREA_CODE region = AREA_CODE.AREA_CODE_GLOB; + + #if (UNITY_2018_3_OR_NEWER && UNITY_ANDROID) + // Define an ArrayList of permissions required for Android devices. + private ArrayList permissionList = new ArrayList() { Permission.Microphone }; + #endif + ``` + + + + + ```javascript + let channelParameters = { + // A variable to hold a local audio track. + localAudioTrack: null, + // A variable to hold a local video track. + localVideoTrack: null, + // A variable to hold a remote audio track. + remoteAudioTrack: null, + // A variable to hold a remote video track. + remoteVideoTrack: null, + // A variable to hold the remote user id.s + remoteUid: null, + }; + + const AgoraRTCManager = async (eventsCallback) => { + let agoraEngine = null; + // Setup done in later steps + }; + ``` + + + ```javascript + let channelParameters = { + // A variable to hold a local audio track. + localAudioTrack: null, + // A variable to hold a remote audio track. + remoteAudioTrack: null, + // A variable to hold the remote user id.s + remoteUid: null, + }; + + const AgoraRTCManager = async (eventsCallback) => { + let agoraEngine = null; + // Setup done in later steps + }; + ``` + + + + ```typescript + const [joined, setJoined] = useState(false); + ``` + + + + ```cpp + IRtcEngine* agoraEngine = nullptr; + std::string appId = ""; + std::string channelName = ""; + std::string token = ""; + u_int remoteUId; + std::shared_ptr AgoraEventStrategy; + int tokenRole = 1; // The token role: Broadcaster or Audience + uid_t uid = 1; // An integer that identifies the user + ``` + + diff --git a/assets/code/video-sdk/get-started-sdk/destroy.mdx b/assets/code/video-sdk/get-started-sdk/destroy.mdx new file mode 100644 index 000000000..bceeeb02c --- /dev/null +++ b/assets/code/video-sdk/get-started-sdk/destroy.mdx @@ -0,0 +1,67 @@ + + ```kotlin + protected fun destroyAgoraEngine() { + // Release the RtcEngine instance to free up resources + RtcEngine.destroy() + agoraEngine = null + } + ``` + + - destroy + + + ```dart + void destroyAgoraEngine() { + // Release the RtcEngine instance to free up resources + if (agoraEngine != null) { + agoraEngine!.release(); + agoraEngine = null; + } + } + ``` + + + ```swift + func destroyAgoraEngine() { + AgoraRtcEngineKit.destroy() + } + ``` + + + - destroy() + + + - destroy() + + + + ```csharp + // Use this function to destroy the engine + public virtual void DestroyEngine() + { + if (agoraEngine != null) + { + // Destroy the engine. + agoraEngine.LeaveChannel(); + agoraEngine.Dispose(); + agoraEngine = null; + } + } + ``` + + - Dispose + + + - Dispose + + + + ```typescript + useEffect(() => { + return () => { + localCameraTrack?.close(); + localMicrophoneTrack?.close(); + }; + }, []); + ``` + diff --git a/assets/code/video-sdk/get-started-sdk/handle-events.mdx b/assets/code/video-sdk/get-started-sdk/handle-events.mdx new file mode 100644 index 000000000..fd439a2cc --- /dev/null +++ b/assets/code/video-sdk/get-started-sdk/handle-events.mdx @@ -0,0 +1,407 @@ + + ```kotlin + protected open val iRtcEngineEventHandler: IRtcEngineEventHandler? + get() = object : IRtcEngineEventHandler() { + // Listen for a remote user joining the channel. + override fun onUserJoined(uid: Int, elapsed: Int) { + sendMessage("Remote user joined $uid") + // Save the uid of the remote user. + remoteUids.add(uid) + if (isBroadcaster && (currentProduct == ProductName.INTERACTIVE_LIVE_STREAMING + || currentProduct == ProductName.BROADCAST_STREAMING) + ) { + // Remote video does not need to be rendered + } else { + // Set up and return a SurfaceView for the new user + setupRemoteVideo(uid) + } + } + + override fun onJoinChannelSuccess(channel: String, uid: Int, elapsed: Int) { + // Set the joined status to true. + isJoined = true + sendMessage("Joined Channel $channel") + // Save the uid of the local user. + localUid = uid + mListener!!.onJoinChannelSuccess(channel, uid, elapsed) + } + + override fun onUserOffline(uid: Int, reason: Int) { + sendMessage("Remote user offline $uid $reason") + // Update the list of remote Uids + remoteUids.remove(uid) + // Notify the UI + mListener!!.onRemoteUserLeft(uid) + } + + override fun onError(err: Int) { + when (err) { + ErrorCode.ERR_TOKEN_EXPIRED -> sendMessage("Your token has expired") + ErrorCode.ERR_INVALID_TOKEN -> sendMessage("Your token is invalid") + else -> sendMessage("Error code: $err") + } + } + } + ``` + + - IRtcEngineEventHandler + - For a list of error codes, see onError + + + + + ```csharp + // Init event handler to receive callbacks + public virtual void InitEventHandler() + { + agoraEngine.InitEventHandler(new UserEventHandler(this)); + } + // An event handler class to deal with video SDK events + internal class UserEventHandler : IRtcEngineEventHandler + { + internal readonly AgoraManager agoraManager; + + internal UserEventHandler(AgoraManager videoSample) + { + agoraManager = videoSample; + } + // This callback is triggered when the local user joins the channel. + public override void OnJoinChannelSuccess(RtcConnection connection, int elapsed) + { + Debug.Log("You joined channel: " +connection.channelId); + } + // This callback is triggered when a remote user leaves the channel or drops offline. + public override void OnUserOffline(RtcConnection connection, uint uid, USER_OFFLINE_REASON_TYPE reason) + { + Debug.Log("OnUserOffline"); + } + public override void OnUserJoined(RtcConnection connection, uint uid, int elapsed) + { + Debug.Log("OnUserJoined"); + } + } + ``` + - OnJoinChannelSuccess + - OnUserOffline + - OnUserJoined + + + ```csharp + // Init event handler to receive callbacks + public virtual void InitEventHandler() + { + agoraEngine.InitEventHandler(new UserEventHandler(this)); + } + // An event handler class to deal with video SDK events + internal class UserEventHandler : IRtcEngineEventHandler + { + internal readonly AgoraManager agoraManager; + + internal UserEventHandler(AgoraManager videoSample) + { + agoraManager = videoSample; + } + // This callback is triggered when the local user joins the channel. + public override void OnJoinChannelSuccess(RtcConnection connection, int elapsed) + { + Debug.Log("You joined channel: " +connection.channelId); + } + + // This callback is triggered when a remote user leaves the channel or drops offline. + public override void OnUserOffline(RtcConnection connection, uint uid, USER_OFFLINE_REASON_TYPE reason) + { + agoraManager.DestroyVideoView(uid); + } + public override void OnUserJoined(RtcConnection connection, uint uid, int elapsed) + { + agoraManager.MakeVideoView(uid, connection.channelId); + // Save the remote user ID in a variable. + agoraManager.remoteUid = uid; + } + } + ``` + - OnJoinChannelSuccess + - OnUserOffline + - OnUserJoined + + + + ```dart + RtcEngineEventHandler getEventHandler() { + return RtcEngineEventHandler( + // Occurs when the network connection state changes + onConnectionStateChanged: (RtcConnection connection, + ConnectionStateType state, ConnectionChangedReasonType reason) { + if (reason == + ConnectionChangedReasonType.connectionChangedLeaveChannel) { + remoteUids.clear(); + isJoined = false; + } + // Notify the UI + Map eventArgs = {}; + eventArgs["connection"] = connection; + eventArgs["state"] = state; + eventArgs["reason"] = reason; + eventCallback("onConnectionStateChanged", eventArgs); + }, + // Occurs when a local user joins a channel + onJoinChannelSuccess: (RtcConnection connection, int elapsed) { + isJoined = true; + messageCallback( + "Local user uid:${connection.localUid} joined the channel"); + // Notify the UI + Map eventArgs = {}; + eventArgs["connection"] = connection; + eventArgs["elapsed"] = elapsed; + eventCallback("onJoinChannelSuccess", eventArgs); + }, + // Occurs when a remote user joins the channel + onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) { + remoteUids.add(remoteUid); + messageCallback("Remote user uid:$remoteUid joined the channel"); + // Notify the UI + Map eventArgs = {}; + eventArgs["connection"] = connection; + eventArgs["remoteUid"] = remoteUid; + eventArgs["elapsed"] = elapsed; + eventCallback("onUserJoined", eventArgs); + }, + // Occurs when a remote user leaves the channel + onUserOffline: (RtcConnection connection, int remoteUid, + UserOfflineReasonType reason) { + remoteUids.remove(remoteUid); + messageCallback("Remote user uid:$remoteUid left the channel"); + // Notify the UI + Map eventArgs = {}; + eventArgs["connection"] = connection; + eventArgs["remoteUid"] = remoteUid; + eventArgs["reason"] = reason; + eventCallback("onUserOffline", eventArgs); + }, + ); + } + ``` + + + +```swift +func rtcEngine( + _ engine: AgoraRtcEngineKit, didJoinChannel channel: String, + withUid uid: UInt, elapsed: Int +) { + // The delegate is telling us that the local user has successfully joined the channel. + self.localUserId = uid + self.allUsers.insert(uid) +} + +func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + // The delegate is telling us that a remote user has joined the channel. + self.allUsers.insert(uid) +} + +func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + // The delegate is telling us that a remote user has left the channel. + self.allUsers.remove(uid) +} +``` + + + +```swift +func rtcEngine( + _ engine: AgoraRtcEngineKit, didJoinChannel channel: String, + withUid uid: UInt, elapsed: Int +) { + // The delegate is telling us that the local user has successfully joined the channel. + self.localUserId = uid + if self.role == .broadcaster { + self.allUsers.insert(uid) + } +} + +func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + // The delegate is telling us that a remote user has joined the channel. + self.allUsers.insert(uid) +} + +func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + // The delegate is telling us that a remote user has left the channel. + self.allUsers.remove(uid) +} +``` + + + + - AgoraRtcEngineDelegate + - rtcEngine(_:didJoinChannel:withUid:elapsed:) + - rtcEngine(_:didJoinedOfUid:elapsed:) + - rtcEngine(_:didOfflineOfUid:reason:) + + + - AgoraRtcEngineDelegate + - rtcEngine(_:didJoinChannel:withUid:elapsed:) + - rtcEngine(_:didJoinedOfUid:elapsed:) + - rtcEngine(_:didOfflineOfUid:reason:) + + + + ```typescript + useClientEvent(agoraEngine, "user-joined", (user) => { + console.log("The user" , user.uid , " has joined the channel"); + }); + + useClientEvent(agoraEngine, "user-left", (user) => { + console.log("The user" , user.uid , " has left the channel"); + }); + + useClientEvent(agoraEngine, "user-published", (user, mediaType) => { + console.log("The user" , user.uid , " has published media in the channel"); + }); + ``` + - useClientEvent + + + + ```javascript + // Event Listeners + agoraEngine.on("user-published", async (user, mediaType) => { + // Subscribe to the remote user when the SDK triggers the "user-published" event. + await agoraEngine.subscribe(user, mediaType); + console.log("subscribe success"); + eventsCallback("user-published", user, mediaType) + }); + + // Listen for the "user-unpublished" event. + agoraEngine.on("user-unpublished", (user) => { + console.log(user.uid + "has left the channel"); + }); + ``` + + The `eventsCallback` callback can be used by the UI to handle all events. The sample project uses the following callback: + + ```javascript + const handleVSDKEvents = (eventName, ...args) => { + switch (eventName) { + case "user-published": + if (args[1] == "video") { + // Retrieve the remote video track. + channelParameters.remoteVideoTrack = args[0].videoTrack; + // Retrieve the remote audio track. + channelParameters.remoteAudioTrack = args[0].audioTrack; + // Save the remote user id for reuse. + channelParameters.remoteUid = args[0].uid.toString(); + // Specify the ID of the DIV container. You can use the uid of the remote user. + remotePlayerContainer.id = args[0].uid.toString(); + channelParameters.remoteUid = args[0].uid.toString(); + remotePlayerContainer.textContent = + "Remote user " + args[0].uid.toString(); + // Append the remote container to the page body. + document.body.append(remotePlayerContainer); + // Play the remote video track. + channelParameters.remoteVideoTrack.play(remotePlayerContainer); + } + // Subscribe and play the remote audio track If the remote user publishes the audio track only. + if (args[1] == "audio") { + // Get the RemoteAudioTrack object in the AgoraRTCRemoteUser object. + channelParameters.remoteAudioTrack = args[0].audioTrack; + // Play the remote audio track. No need to pass any DOM element. + channelParameters.remoteAudioTrack.play(); + } + } + }; + ``` + + + ```javascript + // Event Listeners + agoraEngine.on("user-published", async (user, mediaType) => { + // Subscribe to the remote user when the SDK triggers the "user-published" event. + await agoraEngine.subscribe(user, mediaType); + console.log("subscribe success"); + eventsCallback("user-published", user, mediaType) + }); + + // Listen for the "user-unpublished" event. + agoraEngine.on("user-unpublished", (user) => { + console.log(user.uid + "has left the channel"); + }); + ``` + + The `eventsCallback` callback can be used by the UI to handle all events. The sample project uses the following callback: + + ```javascript + const handleVSDKEvents = (eventName, ...args) => { + switch (eventName) { + case "user-published": + // Subscribe and play the remote audio track If the remote user publishes the audio track only. + if (args[1] == "audio") { + // Get the RemoteAudioTrack object in the AgoraRTCRemoteUser object. + channelParameters.remoteAudioTrack = args[0].audioTrack; + // Play the remote audio track. No need to pass any DOM element. + channelParameters.remoteAudioTrack.play(); + } + } + }; + ``` + + + + + You use `IRtcEngineEventHandler` to implement callback functions. + + ```cpp + class AgoraManagerEventHandler : public IRtcEngineEventHandler + { + public: + // Set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHandler = hWnd; } + + virtual HWND getMsgEventHandler() { return m_hMsgHandler; } + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + virtual void onUserJoined(uid_t uid, int elapsed) override; + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + virtual void onLeaveChannel(const RtcStats& stats) override; + virtual void onTokenPrivilegeWillExpire(const char* token) override; + public: + HWND m_hMsgHandler; + }; + ``` + + Provide definitions for the callbacks you declare in `AgoraEventHandler`. + + ```cpp + void AgoraManagerEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) + { + // Occurs when you join a channel. + } + + void AgoraManagerEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) + { + // Occurs when the remote user drops offline or leave the channel. + MessageBox(NULL, L"Remote user Leave the channel", L"Notification", NULL); + + } + void AgoraManagerEventHandler::onLeaveChannel(const RtcStats& stats) + { + // Occurs when you leave a channel. + MessageBox(NULL, L"You left the channel", L"Notification", NULL); + } + + void AgoraManagerEventHandler::onUserJoined(uid_t uid, int elapsed) + { + HWND MsgEventHandler = getMsgEventHandler(); + // Send a notification to AgoraManager class to setup a remote video view. + if (MsgEventHandler) + { + ::PostMessage(MsgEventHandler, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } + + } + ``` + + - IRtcEngineEventHandler + - onJoinChannelSuccess + - onUserOffline + - onLeaveChannel + + \ No newline at end of file diff --git a/assets/code/video-sdk/get-started-sdk/import-library.mdx b/assets/code/video-sdk/get-started-sdk/import-library.mdx new file mode 100644 index 000000000..097a5a24b --- /dev/null +++ b/assets/code/video-sdk/get-started-sdk/import-library.mdx @@ -0,0 +1,76 @@ + + ```kotlin + import io.agora.rtc2.video.VideoCanvas + import io.agora.rtc2.* + ``` + + + ```dart + import 'dart:convert'; + import 'package:flutter/services.dart'; + import 'package:agora_rtc_engine/agora_rtc_engine.dart'; + import 'package:permission_handler/permission_handler.dart'; + ``` + + + ```swift + import AgoraRtcKit + import SwiftUI + ``` + + + ```javascript + import AgoraRTC from "agora-rtc-sdk-ng"; + import config from "./config.json"; + ``` + + + + ```csharp + using UnityEngine.UI; + using Agora.Rtc; + using System; + using System.IO; + ``` + + + ```typescript + import { + LocalVideoTrack, + RemoteUser, + useJoin, + useLocalCameraTrack, + useLocalMicrophoneTrack, + usePublish, + useRTCClient, + useRemoteUsers, + useClientEvent + } from "agora-rtc-react"; + import { IMicrophoneAudioTrack, ICameraVideoTrack } from "agora-rtc-sdk-ng"; + ``` + + + + From **Solution Explorer**, open the `pch.h` file and add the following lines after `#include "framework.h"`: + + ```cpp + #ifndef PCH_H + #define PCH_H + #define _AFX_ALL_WARNINGS + #include + // For handling config.json for user input + #include + #include + #include + #pragma comment(lib, "agora_rtc_sdk.dll.lib") + #pragma comment(lib, "libagora_segmentation_extension.dll.lib") + #pragma comment(lib, "libagora-ffmpeg.dll.lib") + using namespace agora; + using namespace agora::rtc; + using namespace agora::media; + #define WM_MSGID(code) (WM_USER+0x200+code) + #define EID_USER_JOINED 0x00000003 + #define EID_TOKEN_PRIVILEGE_WILL_EXPIRE 0x00000023 + #endif //PCH_H + ``` + \ No newline at end of file diff --git a/assets/code/video-sdk/get-started-sdk/join-channel.mdx b/assets/code/video-sdk/get-started-sdk/join-channel.mdx new file mode 100644 index 000000000..05bd49b61 --- /dev/null +++ b/assets/code/video-sdk/get-started-sdk/join-channel.mdx @@ -0,0 +1,383 @@ + + + + + ```kotlin + open fun joinChannel(channelName: String, token: String?): Int { + // Ensure that necessary Android permissions have been granted + if (!checkSelfPermission()) { + sendMessage("Permissions were not granted") + return -1 + } + this.channelName = channelName + + // Create an RTCEngine instance + if (agoraEngine == null) setupAgoraEngine() + + val options = ChannelMediaOptions() + + // For a Video/Voice call, set the channel profile as COMMUNICATION. + options.channelProfile = Constants.CHANNEL_PROFILE_COMMUNICATION + // Set the client role to broadcaster or audience + options.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER + // Start local preview. + agoraEngine!!.startPreview() + + // Join the channel using a token. + agoraEngine!!.joinChannel(token, channelName, localUid, options) + return 0 + } + ``` + + + + ```kotlin + open fun joinChannel(channelName: String, token: String?): Int { + // Ensure that necessary Android permissions have been granted + if (!checkSelfPermission()) { + sendMessage("Permissions were not granted") + return -1 + } + this.channelName = channelName + + // Create an RTCEngine instance + if (agoraEngine == null) setupAgoraEngine() + + val options = ChannelMediaOptions() + // For a Video/Voice call, set the channel profile as COMMUNICATION. + options.channelProfile = Constants.CHANNEL_PROFILE_COMMUNICATION + // Set the client role to broadcaster or audience + options.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER + + // Join the channel using a token. + agoraEngine!!.joinChannel(token, channelName, localUid, options) + return 0 + } + ``` + + + + + ```kotlin + open fun joinChannel(channelName: String, token: String?): Int { + // Ensure that necessary Android permissions have been granted + if (!checkSelfPermission()) { + sendMessage("Permissions were not granted") + return -1 + } + this.channelName = channelName + + // Create an RTCEngine instance + if (agoraEngine == null) setupAgoraEngine() + + val options = ChannelMediaOptions() + // Set the channel profile as LIVE_BROADCASTING. + options.channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING + + // Set ultra-low latency for Interactive live streaming + options.audienceLatencyLevel = + Constants.AUDIENCE_LATENCY_LEVEL_ULTRA_LOW_LATENCY + + // Set the client role as BROADCASTER or AUDIENCE according to the scenario. + if (isBroadcaster) { // Broadcasting Host or Video-calling client + options.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER + // Start local preview. + agoraEngine!!.startPreview() + } else { // Audience + options.clientRoleType = Constants.CLIENT_ROLE_AUDIENCE + } + + // Join the channel with a token. + agoraEngine!!.joinChannel(token, channelName, localUid, options) + return 0 + } + ``` + + + + + ```kotlin + open fun joinChannel(channelName: String, token: String?): Int { + // Ensure that necessary Android permissions have been granted + if (!checkSelfPermission()) { + sendMessage("Permissions were not granted") + return -1 + } + this.channelName = channelName + + // Create an RTCEngine instance + if (agoraEngine == null) setupAgoraEngine() + + val options = ChannelMediaOptions() + // Set the channel profile as LIVE_BROADCASTING. + options.channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING + + // Set Low latency for Broadcast streaming + options.audienceLatencyLevel = + Constants.AUDIENCE_LATENCY_LEVEL_LOW_LATENCY + + // Set the client role as BROADCASTER or AUDIENCE according to the scenario. + if (isBroadcaster) { // Broadcasting Host or Video-calling client + options.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER + // Start local preview. + agoraEngine!!.startPreview() + } else { // Audience + options.clientRoleType = Constants.CLIENT_ROLE_AUDIENCE + } + + // Join the channel with a token. + agoraEngine!!.joinChannel(token, channelName, localUid, options) + return 0 + } + ``` + + + - joinChannel + + - ChannelMediaOptions + + - startPreview + + + + + + ```dart + Future join({ + String channelName = '', + String token = '', + int uid = -1, + ClientRoleType clientRole = ClientRoleType.clientRoleBroadcaster, + }) async { + channelName = (channelName.isEmpty) ? this.channelName : channelName; + token = (token.isEmpty) ? config['rtcToken'] : token; + uid = (uid == -1) ? localUid : uid; + + // Set up Agora engine + if (agoraEngine == null) await setupAgoraEngine(); + + // Enable the local video preview + await agoraEngine!.startPreview(); + + // Set channel options including the client role and channel profile + ChannelMediaOptions options = ChannelMediaOptions( + clientRoleType: clientRole, + channelProfile: ChannelProfileType.channelProfileCommunication, + ); + + // Join a channel + await agoraEngine!.joinChannel( + token: token, + channelId: channelName, + options: options, + uid: uid, + ); + } + ``` + + + + ```csharp + public virtual void Join() + { + // Create an instance of the engine. + SetupAgoraEngine(); + + // Setup local video view. + SetupLocalVideo(); + + // Join the channel using the specified token and channel name. + agoraEngine.JoinChannel(configData.rtcToken, configData.channelName); + } + ``` + - JoinChannel + + + ```csharp + public virtual void Join() + { + // Create an instance of the engine. + SetupAgoraEngine(); + + // Join the channel using the specified token and channel name. + agoraEngine.JoinChannel(configData.rtcToken, configData.channelName); + } + ``` + - JoinChannel + + + + + +```swift +func joinVideoCall( + _ channel: String, token: String? = nil, uid: UInt = 0 +) async -> Int32 { + /// See ``AgoraManager/checkForPermissions()``, or Apple's docs for details of this method. + if await !AgoraManager.checkForPermissions() { + await self.updateLabel(key: "invalid-permissions") + return -3 + } + + let opt = AgoraRtcChannelMediaOptions() + opt.channelProfile = .communication + + return self.agoraEngine.joinChannel( + byToken: token, channelId: channel, + uid: uid, mediaOptions: opt + ) +} +``` + + + + +```swift +func joinVoiceCall( + _ channel: String, token: String? = nil, uid: UInt = 0 +) async -> Int32 { + /// See ``AgoraManager/checkForPermissions()``, or Apple's docs for details of this method. + if await !AgoraManager.checkForPermissions() { + await self.updateLabel(key: "invalid-permissions") + return -3 + } + + let opt = AgoraRtcChannelMediaOptions() + opt.channelProfile = .communication + + return self.agoraEngine.joinChannel( + byToken: token, channelId: channel, + uid: uid, mediaOptions: opt + ) +} +``` + + + + +```swift +func joinBroadcastStream( + _ channel: String, token: String? = nil, + uid: UInt = 0, isBroadcaster: Bool = true +) async -> Int32 { + /// See ``AgoraManager/checkForPermissions()``, or Apple's docs for details of this method. + if isBroadcaster, await !AgoraManager.checkForPermissions() { + await self.updateLabel(key: "invalid-permissions") + return -3 + } + + let opt = AgoraRtcChannelMediaOptions() + opt.channelProfile = .liveBroadcasting + opt.clientRoleType = isBroadcaster ? .broadcaster : .audience + opt.audienceLatencyLevel = isBroadcaster ? .ultraLowLatency : .lowLatency + + return self.agoraEngine.joinChannel( + byToken: token, channelId: channel, + uid: uid, mediaOptions: opt + ) +} +``` + + + + + - AgoraRtcChannelMediaOptions + - joinChannel(byToken:channelId:uid:mediaOptions:joinSuccess:) + + + - AgoraRtcChannelMediaOptions + - joinChannel(byToken:channelId:uid:mediaOptions:joinSuccess:) + + + + + ```javascript + const join = async (localPlayerContainer, channelParameters) => { + await agoraEngine.join( + config.appId, + config.channelName, + config.token, + config.uid + ); + // Create a local audio track from the audio sampled by a microphone. + channelParameters.localAudioTrack = + await AgoraRTC.createMicrophoneAudioTrack(); + // Create a local video track from the video captured by a camera. + channelParameters.localVideoTrack = await AgoraRTC.createCameraVideoTrack(); + // Append the local video container to the page body. + document.body.append(localPlayerContainer); + // Publish the local audio and video tracks in the channel. + await getAgoraEngine().publish([ + channelParameters.localAudioTrack, + channelParameters.localVideoTrack, + ]); + // Play the local video track. + channelParameters.localVideoTrack.play(localPlayerContainer); + }; + ``` + - join + - createMicrophoneAudioTrack + - createCameraVideoTrack + - publish + - play + + + ```javascript + const join = async (localPlayerContainer, channelParameters) => { + await agoraEngine.join( + config.appId, + config.channelName, + config.token, + config.uid + ); + // Create a local audio track from the audio sampled by a microphone. + channelParameters.localAudioTrack = + await AgoraRTC.createMicrophoneAudioTrack(); + // Append the local container to the page body. + document.body.append(localPlayerContainer); + // Publish the local audio tracks in the channel. + await getAgoraEngine().publish([ + channelParameters.localAudioTrack, + ]); + }; + ``` + - join + - createMicrophoneAudioTrack + - publish + + + + ```typescript + // Publish local tracks + usePublish([localMicrophoneTrack, localCameraTrack]); + + // Join the Agora channel with the specified configuration + useJoin({ + appid: config.appId, + channel: config.channelName, + token: config.rtcToken, + uid: config.uid, + }); + ``` + - usePublish + + - useJoin + + + + ```cpp + void AgoraManager::join() + { + + if (0 != agoraEngine->joinChannel(token.c_str(), channelName.c_str(), 0, NULL)) + { + MessageBox(NULL, L"AgoraManager::joinChannel() error.", L"Error!", MB_ICONEXCLAMATION | MB_OK); + return; + + } + } + ``` +- joinChannel + + + diff --git a/assets/code/video-sdk/get-started-sdk/leave-channel.mdx b/assets/code/video-sdk/get-started-sdk/leave-channel.mdx new file mode 100644 index 000000000..736c314da --- /dev/null +++ b/assets/code/video-sdk/get-started-sdk/leave-channel.mdx @@ -0,0 +1,145 @@ + + ```kotlin + fun leaveChannel() { + if (!isJoined) { + // Do nothing + } else { + // Call the `leaveChannel` method + agoraEngine!!.leaveChannel() + + // Set the `isJoined` status to false + isJoined = false + // Destroy the engine instance + destroyAgoraEngine() + } + } + ``` + + - leaveChannel + + + + + ```csharp + // Define a public function called Leave() to leave the channel. + public virtual void Leave() + { + if (agoraEngine != null) + { + // Leave the channel and clean up resources + agoraEngine.LeaveChannel(); + agoraEngine.DisableVideo(); + LocalView.SetEnable(false); + DestroyVideoView(remoteUid); + DestroyEngine(); + } + } + ``` + - LeaveChannel + - DisableVideo + - SetEnable + + + ```csharp + // Define a public function called Leave() to leave the channel. + public virtual void Leave() + { + if (agoraEngine != null) + { + // Leave the channel and clean up resources + agoraEngine.LeaveChannel(); + DestroyEngine(); + } + } + ``` + - LeaveChannel + + + + ```dart + Future leave() async { + // Clear saved remote Uids + remoteUids.clear(); + + // Leave the channel + if (agoraEngine != null) { + await agoraEngine!.leaveChannel(); + } + isJoined = false; + + // Destroy the Agora engine instance + destroyAgoraEngine(); + } + ``` + + + ```swift + func leaveChannel( + leaveChannelBlock: ((AgoraChannelStats) -> Void)? = nil + ) -> Int32 { + let leaveErr = self.agoraEngine.leaveChannel(leaveChannelBlock) + self.agoraEngine.stopPreview() + self.allUsers.removeAll() + return leaveErr + } + ``` + + + - leaveChannel(_:) + - stopPreview() + + + - leaveChannel(_:) + - stopPreview() + + + + + ```javascript + const leave = async (channelParameters) => { + // Destroy the local audio and video tracks. + channelParameters.localAudioTrack.close(); + channelParameters.localVideoTrack.close(); + // Remove the containers you created for the local video and remote video. + await agoraEngine.leave(); + }; + ``` + - leave + + + ```javascript + const leave = async (channelParameters) => { + // Destroy the local audio tracks. + channelParameters.localAudioTrack.close(); + await agoraEngine.leave(); + }; + ``` + - leave + + + + + ```cpp + void AgoraManager::leave() + { + // Leave the channel to end the call. + agoraEngine->leaveChannel(); + // Stop the local video preview. + agoraEngine->stopPreview(); + // Disable the local video capturer. + agoraEngine->disableVideo(); + // Disable the local microphone. + agoraEngine->disableAudio(); + } + ``` + + - leaveChannel + + - stopPreview + + - disableAudio + + - disableVideo + + + diff --git a/assets/code/video-sdk/get-started-sdk/local-video.mdx b/assets/code/video-sdk/get-started-sdk/local-video.mdx new file mode 100644 index 000000000..798c4fe19 --- /dev/null +++ b/assets/code/video-sdk/get-started-sdk/local-video.mdx @@ -0,0 +1,123 @@ + + ```kotlin + val localVideo: SurfaceView + get() { + // Create a SurfaceView object for the local video + val localSurfaceView = SurfaceView(mContext) + localSurfaceView.visibility = View.VISIBLE + // Call setupLocalVideo with a VideoCanvas having uid set to 0. + agoraEngine!!.setupLocalVideo( + VideoCanvas( + localSurfaceView, + VideoCanvas.RENDER_MODE_HIDDEN, + 0 + ) + ) + return localSurfaceView + } + ``` + + - setupLocalVideo + + + + ```dart + AgoraVideoView localVideoView() { + return AgoraVideoView( + controller: VideoViewController( + rtcEngine: agoraEngine!, + canvas: const VideoCanvas(uid: 0), // Use uid = 0 for local view + ), + ); + } + ``` + + + ```swift + func createLocalCanvasView() { + // Create and return the video view + var canvas = AgoraRtcVideoCanvas() + let canvasView = UIView() + canvas.view = canvasView + + agoraEngine.startPreview() + agoraEngine.setupLocalVideo(canvas) + } + ``` + + - AgoraRtcVideoCanvas + - startPreview() + - setupLocalVideo(_:) + + See [`AgoraVideoCanvasView`](https://github.com/AgoraIO/video-sdk-samples-ios/blob/main/agora-manager/AgoraVideoCanvasView.swift) for a full implementation. + + + + ```csharp + public virtual void SetupLocalVideo() + { + // Set the local video view. + LocalView.SetForUser(configData.uid, _channelName); + + // Start rendering local video. + LocalView.SetEnable(true); + + } + ``` + - SetForUser + - SetEnable + + + ```swift + func createLocalCanvasView() { + // Create and return the video view + var canvas = AgoraRtcVideoCanvas() + let canvasView = NSView() + canvas.view = canvasView + + agoraEngine.startPreview() + agoraEngine.setupLocalVideo(canvas) + } + ``` + + - AgoraRtcVideoCanvas + - startPreview() + - setupLocalVideo(_:) + + See [`AgoraVideoCanvasView`](https://github.com/AgoraIO/video-sdk-samples-macos/blob/main/agora-manager/AgoraVideoCanvasView.swift) for a full implementation. + + + + ```javascript + + ``` + + + ```typescript +
+ +
+ ``` + - LocalAudioTrack +
+ + + ```cpp + // Setup a video canvas to render the local video. + VideoCanvas canvas; + // Assign the local user ID to canvas for identification. + canvas.uid = 1; + // Pass the local view window handle to canvas to render the local video. + canvas.view = gui->localView; + // Select a local video source. + canvas.sourceType = VIDEO_SOURCE_CAMERA; + // Render the local video. + agoraEngine->setupLocalVideo(canvas); + //agora::rtc::uid_t uid = this->uid; + // Preview the local video. + agoraEngine->startPreview(); + ``` + - setupLocalVideo + + - startPreview + diff --git a/assets/code/video-sdk/get-started-sdk/remote-video.mdx b/assets/code/video-sdk/get-started-sdk/remote-video.mdx new file mode 100644 index 000000000..233b8619d --- /dev/null +++ b/assets/code/video-sdk/get-started-sdk/remote-video.mdx @@ -0,0 +1,153 @@ + + ```kotlin + protected fun setupRemoteVideo(remoteUid: Int) { + // Create a new SurfaceView + val remoteSurfaceView = SurfaceView(mContext) + remoteSurfaceView.setZOrderMediaOverlay(true) + // Create a VideoCanvas using the remoteSurfaceView + val videoCanvas = VideoCanvas( + remoteSurfaceView, + VideoCanvas.RENDER_MODE_FIT, remoteUid + ) + agoraEngine!!.setupRemoteVideo(videoCanvas) + // Set the visibility + remoteSurfaceView.visibility = View.VISIBLE + // Notify the UI to display the video + mListener!!.onRemoteUserJoined(remoteUid, remoteSurfaceView) + } + ``` + - VideoCanvas + - setupRemoteVideo + + + + ```csharp + // Dynamically create views for the remote users + public void MakeVideoView(uint uid, string channelName) + { + // Create and configure a remote user's video view + AgoraUI agoraUI = new AgoraUI(); + GameObject userView = agoraUI.MakeRemoteView(uid.ToString()); + userView.AddComponent(); + + VideoSurface videoSurface = userView.AddComponent(); + videoSurface.SetForUser(uid, channelName, VIDEO_SOURCE_TYPE.VIDEO_SOURCE_REMOTE); + videoSurface.OnTextureSizeModify += (int width, int height) => + { + float scale = (float)height / (float)width; + videoSurface.transform.localScale = new Vector3(-5, 5 * scale, 1); + Debug.Log("OnTextureSizeModify: " + width + " " + height); + }; + videoSurface.SetEnable(true); + + RemoteView = videoSurface; + } + + // Destroy the remote user's video view when they leave + public void DestroyVideoView(uint uid) + { + var userView = GameObject.Find(uid.ToString()); + if (!ReferenceEquals(userView, null)) + { + userView.SetActive(false); // Deactivate the GameObject + } + } + ``` + - VideoSurface + - SetForUser + + + + ```dart + AgoraVideoView remoteVideoView(int remoteUid) { + return AgoraVideoView( + controller: VideoViewController.remote( + rtcEngine: agoraEngine!, + canvas: VideoCanvas(uid: remoteUid), + connection: RtcConnection(channelId: channelName), + ), + ); + } + ``` + + + ```swift + func createRemoteCanvasView(with uid: UInt) { + // Create and return the video view + var canvas = AgoraRtcVideoCanvas() + canvas.uid = uid + let canvasView = UIView() + canvas.view = canvasView + + agoraEngine.setupRemoteVideo(canvas) + } + ``` + + - AgoraRtcVideoCanvas + - setupRemoteVideo(_:) + +See [`AgoraVideoCanvasView`](https://github.com/AgoraIO/video-sdk-samples-ios/blob/main/agora-manager/AgoraVideoCanvasView.swift) for a full implementation. + + + + ```swift + func createRemoteCanvasView(with uid: UInt) { + // Create and return the video view + var canvas = AgoraRtcVideoCanvas() + canvas.uid = uid + let canvasView = NSView() + canvas.view = canvasView + + agoraEngine.setupRemoteVideo(canvas) + } + ``` + + - AgoraRtcVideoCanvas + - setupRemoteVideo(_:) + +See [`AgoraVideoCanvasView`](https://github.com/AgoraIO/video-sdk-samples-macos/blob/main/agora-manager/AgoraVideoCanvasView.swift) for a full implementation. + + + + ```typescript + const remoteUsers = useRemoteUsers(); + {remoteUsers.map((remoteUser) => ( +
+ +
+ ))} + ``` + - RemoteVideoTrack +
+ + + ```cpp + LRESULT AgoraManager::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) + { + // Setup a video canvas to render the remote video. + VideoCanvas canvas; + // Choose a video render mode. + canvas.renderMode = media::base::RENDER_MODE_FIT; + // Assign the remote user ID to the canvas for identification. + canvas.uid = wParam; + // Pass the remote view window handle to canvas to render the remote video. + canvas.view = gui->remoteView; + // Render the remote video. + agoraEngine->setupRemoteVideo(canvas); + // Save the remote user ID for reuse. + remoteUId = wParam; + // Notify the parent window + HWND hwndParent = GetParent(gui->getGuiWindowReference()); + + if (hwndParent != NULL) + { + PostMessage(hwndParent, WM_MSGID(EID_USER_JOINED), TRUE, 0); + } + + return 0; + } + ``` + - onUserJoined + + - setupRemoteVideo + diff --git a/assets/code/video-sdk/get-started-sdk/request-permissions.mdx b/assets/code/video-sdk/get-started-sdk/request-permissions.mdx new file mode 100644 index 000000000..e44d6a293 --- /dev/null +++ b/assets/code/video-sdk/get-started-sdk/request-permissions.mdx @@ -0,0 +1,102 @@ + + + ```kotlin + companion object { + protected const val PERMISSION_REQ_ID = 22 + protected val REQUESTED_PERMISSIONS = arrayOf( + Manifest.permission.RECORD_AUDIO, + Manifest.permission.CAMERA + ) + } + + protected fun checkSelfPermission(): Boolean { + return ContextCompat.checkSelfPermission( + mContext, + REQUESTED_PERMISSIONS[0] + ) == PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission( + mContext, + REQUESTED_PERMISSIONS[1] + ) == PackageManager.PERMISSION_GRANTED + } + + if (!checkSelfPermission()) { + ActivityCompat.requestPermissions(activity, REQUESTED_PERMISSIONS, PERMISSION_REQ_ID) + } + ``` + + + ```kotlin + companion object { + protected const val PERMISSION_REQ_ID = 22 + protected val REQUESTED_PERMISSIONS = arrayOf( + Manifest.permission.RECORD_AUDIO + ) + } + + protected fun checkSelfPermission(): Boolean { + return ContextCompat.checkSelfPermission( + mContext, + REQUESTED_PERMISSIONS[0] + ) == PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission( + mContext, + REQUESTED_PERMISSIONS[1] + ) == PackageManager.PERMISSION_GRANTED + } + + if (!checkSelfPermission()) { + ActivityCompat.requestPermissions(activity, REQUESTED_PERMISSIONS, PERMISSION_REQ_ID) + } + ``` + + + + + + ```csharp + // Define a private function called CheckPermissions() to check for required permissions. + public void CheckPermissions() + { + #if (UNITY_2018_3_OR_NEWER && UNITY_ANDROID) + // Check for each permission in the permission list and request the user to grant it if necessary. + foreach (string permission in permissionList) + { + if (!Permission.HasUserAuthorizedPermission(permission)) + { + Permission.RequestUserPermission(permission); + } + } + #endif + } + ``` + + + ```swift + static func checkForPermissions() async -> Bool { + var hasPermissions = await self.avAuthorization(mediaType: .video) + // Break out, because camera permissions have been denied or restricted. + if !hasPermissions { return false } + hasPermissions = await self.avAuthorization(mediaType: .audio) + return hasPermissions + } + + static func avAuthorization(mediaType: AVMediaType) async -> Bool { + let mediaAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: mediaType) + switch mediaAuthorizationStatus { + case .denied, .restricted: return false + case .authorized: return true + case .notDetermined: + return await withCheckedContinuation { continuation in + AVCaptureDevice.requestAccess(for: mediaType) { granted in + continuation.resume(returning: granted) + } + } + @unknown default: return false + } + } + ``` + + - authorizationStatus(for:) + - requestAccess(for:completionHandler:) + \ No newline at end of file diff --git a/assets/code/video-sdk/get-started-sdk/set-user-role.mdx b/assets/code/video-sdk/get-started-sdk/set-user-role.mdx new file mode 100644 index 000000000..63adbe703 --- /dev/null +++ b/assets/code/video-sdk/get-started-sdk/set-user-role.mdx @@ -0,0 +1,88 @@ + + + ```csharp + public virtual void SetClientRole(string role) + { + if(agoraEngine == null) + { + Debug.Log("Click join and then change the client role!"); + return; + } + userRole = role; + if (role == "Host") + { + agoraEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER); + } + else if(role == "Audience") + { + agoraEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_AUDIENCE); + } + } + ``` + - SetClientRole + + + + + ```typescript + const agoraEngine = useRTCClient(); + const handleRoleChange = (event: React.ChangeEvent) => + { + setRole(event.target.value); + if(event.target.value === "host") + { + agoraEngine.setClientRole("host").then(() => { + // Your code to handle the resolution of the promise + console.log("Client role set to host successfully"); + }).catch((error) => { + // Your code to handle any errors + console.error("Error setting client role:", error); + }); + } + else + { + agoraEngine.setClientRole("audience").then(() => { + // Your code to handle the resolution of the promise + console.log("Client role set to host successfully"); + }).catch((error) => { + // Your code to handle any errors + console.error("Error setting client role:", error); + }); + } + }; + ``` + + + + ```typescript + // Only valid when the channel profile is set to "live" + const setUserRole = (role) => { + if(role == "audience") + { + // An audience can only receive audio or video + // AUDIENCE_LEVEL_ULTRA_LOW_LATENCY takes effect only when the user role is "audience". + agoraEngine.setClientRole(role, AudienceLatencyLevelType.AUDIENCE_LEVEL_ULTRA_LOW_LATENCY); + } + else + { + // A host can send and receive audio or video + agoraEngine.setClientRole(role, null); + } + } + ``` + - setClientRole + + + ```swift + agoraEngine.setClientRole(role) + ``` + + + - setClientRole(_:) + - AgoraClientRole + + + - setClientRole(_:) + - AgoraClientRole + + diff --git a/assets/code/video-sdk/get-started-sdk/setup-audio-video-tracks.mdx b/assets/code/video-sdk/get-started-sdk/setup-audio-video-tracks.mdx new file mode 100644 index 000000000..7eccdd628 --- /dev/null +++ b/assets/code/video-sdk/get-started-sdk/setup-audio-video-tracks.mdx @@ -0,0 +1,9 @@ + + ```typescript + const { isLoading: isLoadingCam, localCameraTrack } = useLocalCameraTrack(); + const { isLoading: isLoadingMic, localMicrophoneTrack } = useLocalMicrophoneTrack(); + ``` + - useLocalCameraTrack + - useLocalMicrophoneTrack + + \ No newline at end of file diff --git a/assets/code/video-sdk/get-started-sdk/swift/create-ui.mdx b/assets/code/video-sdk/get-started-sdk/swift/create-ui.mdx deleted file mode 100644 index 6ceef5810..000000000 --- a/assets/code/video-sdk/get-started-sdk/swift/create-ui.mdx +++ /dev/null @@ -1,278 +0,0 @@ - - - -``` swift -import Cocoa -import AppKit -import Foundation - -class ViewController: NSViewController { - - // The video feed for the local user is displayed here - var localView: NSView! - // The video feed for the remote user is displayed here - var remoteView: NSView! - // Click to join or leave a call - var joinButton: NSButton! - // Track if the local user is in a call - var joined: Bool = false - - override func viewDidLoad() { - super.viewDidLoad() - initViews() - } - - func joinChannel() - - func leaveChannel() {} - - func initViews() { - - // Initializes the remote video view. This view displays video when a remote host joins the channel. - remoteView = NSView() - remoteView.frame = CGRect(x: 20, y: 80, width: 150, height: 150) - self.view.addSubview(remoteView) - localView = NSView() - localView.frame = CGRect(x: 300, y: 80, width: 150, height: 150) - self.view.addSubview(localView) - - joinButton = NSButton() - joinButton.frame = CGRect(x: 200 , y: 10, width: 50, height: 20) - joinButton.title = "Join" - joinButton.target = self - joinButton.action = #selector(buttonAction) - self.view.addSubview(joinButton) - } - - - @objc func buttonAction(sender: NSButton!) { - if !joined { - joinChannel() - // Check if successfully joined the channel and set button title accordingly - if joined { joinButton.title = "Leave" } - } else { - leaveChannel() - // Check if successfully left the channel and set button title accordingly - if !joined { joinButton.title = "Join"} - } - } - -} -``` - - - -``` swift -import Cocoa -import AppKit -import Foundation - -class ViewController: NSViewController { - - // The video feed for the local user is displayed here - var localView: NSView! - // The video feed for the remote user is displayed here - var remoteView: NSView! - // Click to join or leave a call - var joinButton: NSButton! - // Choose to be broadcaster or audience - var role: NSSegmentedControl! - // Track if the local user is in a call - var joined: Bool = false - - override func viewDidLoad() { - super.viewDidLoad() - initViews() - } - - func joinChannel() - - func leaveChannel() {} - - func initViews() { - - // Initializes the remote video view. This view displays video when a remote host joins the channel. - remoteView = NSView() - remoteView.frame = CGRect(x: 20, y: 80, width: 150, height: 150) - self.view.addSubview(remoteView) - localView = NSView() - localView.frame = CGRect(x: 300, y: 80, width: 150, height: 150) - self.view.addSubview(localView) - - joinButton = NSButton() - joinButton.frame = CGRect(x: 200 , y:10, width:50, height:20) - joinButton.title = "Join" - joinButton.target = self - joinButton.action = #selector(buttonAction) - self.view.addSubview(joinButton) - - // Selector to be the host or the audience - role = NSSegmentedControl( - labels: ["Broadcast", "Audience"], trackingMode: .selectOne, target: self, - action: #selector(roleAction) - ) - role.frame = CGRect(x: 20, y: 10, width: 160, height: 20) - role.selectedSegment = 0 - self.view.addSubview(role) - } - - - @objc func buttonAction(sender: NSButton!) { - if !joined { - joinChannel() - // Check if successfully joined the channel and set button title accordingly - if joined { joinButton.title = "Leave" } - } else { - leaveChannel() - // Check if successfully left the channel and set button title accordingly - if !joined { joinButton.title = "Join"} - } - } - - @objc func roleAction(sender: NSSegmentedControl!) {} -} -``` - - - - - - - ``` swift - import UIKit - import AVFoundation - - class ViewController: UIViewController { - // The video feed for the local user is displayed here - var localView: UIView! - // The video feed for the remote user is displayed here - var remoteView: UIView! - // Click to join or leave a call - var joinButton: UIButton! - - // Track if the local user is in a call - var joined: Bool = false { - didSet { - DispatchQueue.main.async { - self.joinButton.setTitle( self.joined ? "Leave" : "Join", for: .normal) - } - } - } - - override func viewDidLoad() { - super.viewDidLoad() - initViews() - } - - func joinChannel() async { } - - func leaveChannel() {} - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - remoteView.frame = CGRect(x: 20, y: 50, width: 350, height: 330) - localView.frame = CGRect(x: 20, y: 400, width: 350, height: 330) - } - - func initViews() { - // Initializes the remote video view. This view displays video when a remote host joins the channel. - remoteView = UIView() - self.view.addSubview(remoteView) - // Initializes the local video window. This view displays video when the local user is a host. - localView = UIView() - self.view.addSubview(localView) - // Button to join or leave a channel - joinButton = UIButton(type: .system) - joinButton.frame = CGRect(x: 140, y: 700, width: 100, height: 50) - joinButton.setTitle("Join", for: .normal) - - joinButton.addTarget(self, action: #selector(buttonAction), for: .touchUpInside) - self.view.addSubview(joinButton) - } - - @objc func buttonAction(sender: UIButton!) { - if !joined { - sender.isEnabled = false - Task { - await joinChannel() - sender.isEnabled = true - } - } else { - leaveChannel() - } - } - } - ``` - - - ``` swift - import AVFoundation - import UIKit - - class ViewController: UIViewController { - // The video feed for the local user is displayed here - var localView: UIView! - // The video feed for the remote user is displayed here - var remoteView: UIView! - // Click to join or leave a call - var joinButton: UIButton! - // Choose to be broadcaster or audience - var role: UISegmentedControl! - // Track if the local user is in a call - var joined: Bool = false - - override func viewDidLoad() { - super.viewDidLoad() - initViews() - } - - func joinChannel() async { } - - func leaveChannel() {} - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - remoteView.frame = CGRect(x: 20, y: 50, width: 350, height: 330) - localView.frame = CGRect(x: 20, y: 400, width: 350, height: 330) - } - - func initViews() { - // Initializes the remote video view. This view displays video when a remote host joins the channel. - remoteView = UIView() - self.view.addSubview(remoteView) - // Initializes the local video window. This view displays video when the local user is a host. - localView = UIView() - self.view.addSubview(localView) - // Button to join or leave a channel - joinButton = UIButton(type: .system) - joinButton.frame = CGRect(x: 140, y: 700, width: 100, height: 50) - joinButton.setTitle("Join", for: .normal) - - joinButton.addTarget(self, action: #selector(buttonAction), for: .touchUpInside) - self.view.addSubview(joinButton) - - // Selector to be the host or the audience - role = UISegmentedControl(items: ["Broadcast", "Audience"]) - role.frame = CGRect(x: 20, y: 740, width: 350, height: 40) - role.selectedSegmentIndex = 0 - role.addTarget(self, action: #selector(roleAction), for: .valueChanged) - self.view.addSubview(role) - } - - @objc func buttonAction(sender: UIButton!) { - if !joined { - sender.isEnabled = false - Task { - await joinChannel() - sender.isEnabled = true - } - } else { - leaveChannel() - } - } - - @objc func roleAction(sender: UISegmentedControl!) {} - } - ``` - - \ No newline at end of file diff --git a/assets/code/video-sdk/get-started-sdk/swift/handle-events.mdx b/assets/code/video-sdk/get-started-sdk/swift/handle-events.mdx deleted file mode 100644 index 316764023..000000000 --- a/assets/code/video-sdk/get-started-sdk/swift/handle-events.mdx +++ /dev/null @@ -1,17 +0,0 @@ - -``` swift -open func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { - self.localUserId = uid - if self.role == .broadcaster { - self.allUsers.insert(uid) - } -} - -open func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { - self.allUsers.insert(uid) -} - -open func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { - self.allUsers.remove(uid) -} -``` diff --git a/assets/code/video-sdk/get-started-sdk/swift/join-and-leave.mdx b/assets/code/video-sdk/get-started-sdk/swift/join-and-leave.mdx deleted file mode 100644 index 76abf28e5..000000000 --- a/assets/code/video-sdk/get-started-sdk/swift/join-and-leave.mdx +++ /dev/null @@ -1,239 +0,0 @@ - - - - ``` swift - func joinChannel() { - - let option = AgoraRtcChannelMediaOptions() - - // Set the client role option as broadcaster or audience. - if self.userRole == .broadcaster { - option.clientRoleType = .broadcaster - setupLocalVideo() - } else { - option.clientRoleType = .audience - } - - // For a video call scenario, set the channel profile as communication. - option.channelProfile = .communication - - // Join the channel with a temp token. Pass in your token and channel name here - let result = agoraEngine.joinChannel( - byToken: token, channelId: channelName, uid: 0, mediaOptions: option, - joinSuccess: { (channel, uid, elapsed) in } - ) - // Check if joining the channel was successful and set joined Bool accordingly - if result == 0 { - joined = true - showMessage(title: "Success", text: "Successfully joined the channel as \(self.userRole)") - } - } - - func leaveChannel() { - agoraEngine.stopPreview() - let result = agoraEngine.leaveChannel(nil) - // Check if leaving the channel was successful and set joined Bool accordingly - if result == 0 { joined = false } - } - ``` - - - - ``` swift - func joinChannel() { - - let option = AgoraRtcChannelMediaOptions() - - // Set the client role option as broadcaster or audience. - if self.userRole == .broadcaster { - option.clientRoleType = .broadcaster - setupLocalVideo() - } else { - option.clientRoleType = .audience - } - // For a live streaming scenario, set the channel profile as liveBroadcasting. - option.channelProfile = .liveBroadcasting - // Join the channel with a temp token. Pass in your token and channel name here - let result = agoraEngine.joinChannel( - byToken: token, channelId: channelName, uid: 0, mediaOptions: option, - joinSuccess: { (channel, uid, elapsed) in } - ) - // Check if joining the channel was successful and set joined Bool accordingly - if result == 0 { - joined = true - showMessage(title: "Success", text: "Successfully joined the channel as \(self.userRole)") - } - } - - func leaveChannel() { - agoraEngine.stopPreview() - let result = agoraEngine.leaveChannel(nil) - // Check if leaving the channel was successful and set joined Bool accordingly - if result == 0 { joined = false } - } - ``` - - - ``` swift - func joinChannel() { - - let option = AgoraRtcChannelMediaOptions() - - // Set the client role option as broadcaster or audience. - if self.userRole == .broadcaster { - option.clientRoleType = .broadcaster - setupLocalVideo() - } else { - option.clientRoleType = .audience - option.audienceLatencyLevel = .lowLatency - } - // For a live streaming scenario, set the channel profile as liveBroadcasting. - option.channelProfile = .liveBroadcasting - // Join the channel with a temp token. Pass in your token and channel name here - let result = agoraEngine.joinChannel( - byToken: token, channelId: channelName, uid: 0, mediaOptions: option, - joinSuccess: { (channel, uid, elapsed) in } - ) - // Check if joining the channel was successful and set joined Bool accordingly - if result == 0 { - joined = true - showMessage(title: "Success", text: "Successfully joined the channel as \(self.userRole)") - } - } - - func leaveChannel() { - agoraEngine.stopPreview() - let result = agoraEngine.leaveChannel(nil) - // Check if leaving the channel was successful and set joined Bool accordingly - if (result == 0) { joined = false } - } - ``` - For , use `AgoraAudienceLatencyLevelLowLatency` for audience roles. This ensures low latency, which is a feature of and its use is subject to special [pricing](../reference/pricing#unit-pricing). - - - - - - - ``` swift - func joinChannel() async { - if await !self.checkForPermissions() { - showMessage(title: "Error", text: "Permissions were not granted") - return - } - - let option = AgoraRtcChannelMediaOptions() - - // Set the client role option as broadcaster or audience. - if self.userRole == .broadcaster { - option.clientRoleType = .broadcaster - setupLocalVideo() - } else { - option.clientRoleType = .audience - } - - // For a video call scenario, set the channel profile as communication. - option.channelProfile = .communication - - // Join the channel with a temp token. Pass in your token and channel name here - let result = agoraEngine.joinChannel( - byToken: token, channelId: channelName, uid: 0, mediaOptions: option, - joinSuccess: { (channel, uid, elapsed) in } - ) - // Check if joining the channel was successful and set joined Bool accordingly - if result == 0 { - joined = true - showMessage(title: "Success", text: "Successfully joined the channel as \(self.userRole)") - } - } - - func leaveChannel() { - agoraEngine.stopPreview() - let result = agoraEngine.leaveChannel(nil) - // Check if leaving the channel was successful and set joined Bool accordingly - if result == 0 { joined = false } - } - ``` - - - - ``` swift - func joinChannel() async { - if await !self.checkForPermissions() { - showMessage(title: "Error", text: "Permissions were not granted") - return - } - - let option = AgoraRtcChannelMediaOptions() - - // Set the client role option as broadcaster or audience. - if self.userRole == .broadcaster { - option.clientRoleType = .broadcaster - setupLocalVideo() - } else { - option.clientRoleType = .audience - } - // For a live streaming scenario, set the channel profile as liveBroadcasting. - option.channelProfile = .liveBroadcasting - // Join the channel with a temp token. Pass in your token and channel name here - let result = agoraEngine.joinChannel( - byToken: token, channelId: channelName, uid: 0, mediaOptions: option, - joinSuccess: { (channel, uid, elapsed) in } - ) - // Check if joining the channel was successful and set joined Bool accordingly - if result == 0 { - joined = true - showMessage(title: "Success", text: "Successfully joined the channel as \(self.userRole)") - } - } - - func leaveChannel() { - agoraEngine.stopPreview() - let result = agoraEngine.leaveChannel(nil) - // Check if leaving the channel was successful and set joined Bool accordingly - if result == 0 { joined = false } - } - ``` - - - ``` swift - func joinChannel() { - if !self.checkForPermissions() { - showMessage(title: "Error", text: "Permissions were not granted") - return - } - - let option = AgoraRtcChannelMediaOptions() - - // Set the client role option as broadcaster or audience. - if self.userRole == .broadcaster { - option.clientRoleType = .broadcaster - setupLocalVideo() - } else { - option.clientRoleType = .audience - option.audienceLatencyLevel = .lowLatency - } - // For a live streaming scenario, set the channel profile as liveBroadcasting. - option.channelProfile = .liveBroadcasting - // Join the channel with a temp token. Pass in your token and channel name here - let result = agoraEngine.joinChannel( - byToken: token, channelId: channelName, uid: 0, mediaOptions: option, - joinSuccess: { (channel, uid, elapsed) in } - ) - // Check if joining the channel was successful and set joined Bool accordingly - if result == 0 { - joined = true - showMessage(title: "Success", text: "Successfully joined the channel as \(self.userRole)") - } - } - - func leaveChannel() { - agoraEngine.stopPreview() - let result = agoraEngine.leaveChannel(nil) - // Check if leaving the channel was successful and set joined Bool accordingly - if (result == 0) { joined = false } - } - ``` - For , use `AgoraAudienceLatencyLevelLowLatency` for audience roles. This ensures low latency, which is a feature of and its use is subject to special [pricing](../reference/pricing#unit-pricing). - - \ No newline at end of file diff --git a/assets/code/video-sdk/get-started-sdk/swift/join-channel.mdx b/assets/code/video-sdk/get-started-sdk/swift/join-channel.mdx deleted file mode 100644 index 085cad811..000000000 --- a/assets/code/video-sdk/get-started-sdk/swift/join-channel.mdx +++ /dev/null @@ -1,39 +0,0 @@ - -``` swift -open func joinChannel( - _ channel: String, token: String? = nil, uid: UInt = 0, info: String? = nil -) async -> Int32 { - if await !AgoraManager.checkForPermissions() { - DispatchQueue.main.async { - self.label = """ - Camera and microphone permissions were not granted. - Check your security settings and try again. - """ - } - return -3 - } - - return self.agoraEngine.joinChannel( - byToken: token, channelId: channel, - info: info, uid: uid - ) -} - -// This method is specifically used by the sample app. If there is a tokenURL, it will attempt to retrieve a token from there. -internal func joinChannel(_ channel: String, uid: UInt? = nil) async -> Int32 { - let userId = uid ?? DocsAppConfig.shared.uid - var token = DocsAppConfig.shared.rtcToken - if !DocsAppConfig.shared.tokenUrl.isEmpty { - do { - token = try await self.fetchToken( - from: DocsAppConfig.shared.tokenUrl, channel: channel, - role: self.role, userId: userId - ) - } catch { - print("token server fetch failed: \(error.localizedDescription)") - } - } - return await self.joinChannel(channel, token: token, uid: userId, info: nil) -} -``` - diff --git a/assets/code/video-sdk/get-started-sdk/swift/leave-channel.mdx b/assets/code/video-sdk/get-started-sdk/swift/leave-channel.mdx deleted file mode 100644 index 8a057b528..000000000 --- a/assets/code/video-sdk/get-started-sdk/swift/leave-channel.mdx +++ /dev/null @@ -1,13 +0,0 @@ - -``` swift -open func leaveChannel( - leaveChannelBlock: ((AgoraChannelStats) -> Void)? = nil, - destroyInstance: Bool = true -) -> Int32 { - let leaveErr = self.agoraEngine.leaveChannel(leaveChannelBlock) - self.agoraEngine.stopPreview() - defer { if destroyInstance { AgoraRtcEngineKit.destroy() } } - self.allUsers.removeAll() - return leaveErr -} -``` \ No newline at end of file diff --git a/assets/code/video-sdk/get-started-sdk/swift/role-action.mdx b/assets/code/video-sdk/get-started-sdk/swift/role-action.mdx deleted file mode 100644 index 3451eb33e..000000000 --- a/assets/code/video-sdk/get-started-sdk/swift/role-action.mdx +++ /dev/null @@ -1,14 +0,0 @@ - -``` swift -@objc func roleAction(sender: NSSegmentedControl!) { - self.userRole = sender.selectedSegment == 0 ? .broadcaster : .audience -} -``` - - -``` swift -@objc func roleAction(sender: UISegmentedControl!) { - self.userRole = sender.selectedSegmentIndex == 0 ? .broadcaster : .audience -} -``` - \ No newline at end of file diff --git a/assets/code/video-sdk/get-started-sdk/swift/show-message.mdx b/assets/code/video-sdk/get-started-sdk/swift/show-message.mdx deleted file mode 100644 index fc02562b3..000000000 --- a/assets/code/video-sdk/get-started-sdk/swift/show-message.mdx +++ /dev/null @@ -1,26 +0,0 @@ - -``` swift -func showMessage(title: String, text: String, delay: Int = 2) -> Void { - let deadlineTime = DispatchTime.now() + .seconds(delay) - DispatchQueue.main.asyncAfter(deadline: deadlineTime, execute: { - let alert: NSAlert = NSAlert() - alert.messageText = title - alert.informativeText = text - alert.alertStyle = .informational - alert.runModal() - }) -} -``` - - -``` swift -func showMessage(title: String, text: String, delay: Int = 2) -> Void { - let deadlineTime = DispatchTime.now() + .seconds(delay) - DispatchQueue.main.asyncAfter(deadline: deadlineTime, execute: { - let alert = UIAlertController(title: title, message: text, preferredStyle: .alert) - self.present(alert, animated: true) - alert.dismiss(animated: true, completion: nil) - }) -} -``` - \ No newline at end of file diff --git a/assets/code/video-sdk/get-started-sdk/swift/view-did-disappear.mdx b/assets/code/video-sdk/get-started-sdk/swift/view-did-disappear.mdx deleted file mode 100644 index f1055a74e..000000000 --- a/assets/code/video-sdk/get-started-sdk/swift/view-did-disappear.mdx +++ /dev/null @@ -1,19 +0,0 @@ - -``` swift - override func viewDidDisappear() - { - super.viewDidDisappear() - leaveChannel() - DispatchQueue.global(qos: .userInitiated).async {AgoraRtcEngineKit.destroy()} - } -``` - - -``` swift - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - leaveChannel() - DispatchQueue.global(qos: .userInitiated).async {AgoraRtcEngineKit.destroy()} - } -``` - \ No newline at end of file diff --git a/assets/code/video-sdk/live-streaming-multiple-channels/import-library.mdx b/assets/code/video-sdk/live-streaming-multiple-channels/import-library.mdx new file mode 100644 index 000000000..9a26cf75d --- /dev/null +++ b/assets/code/video-sdk/live-streaming-multiple-channels/import-library.mdx @@ -0,0 +1,43 @@ + + ```kotlin + import io.agora.rtc2.* + import io.agora.rtc2.video.ChannelMediaInfo + import io.agora.rtc2.video.ChannelMediaRelayConfiguration + import io.agora.rtc2.video.VideoCanvas + ``` + + + ```swift + import AgoraRtcKit + ``` + + + ```csharp + using Agora.Rtc; + ``` + + + ```typescript + import { + AgoraRTCProvider, + useRTCClient, + useConnectionState, + usePublish, + useJoin, + useRemoteUsers, + RemoteUser, + useLocalCameraTrack, + useLocalMicrophoneTrack + } from "agora-rtc-react"; + import AgoraRTC, { IAgoraRTCClient, ChannelMediaRelayState, ChannelMediaRelayError, ChannelMediaRelayEvent } from "agora-rtc-sdk-ng"; + import config from "../agora-manager/config"; + import {useState} from 'react'; + import AuthenticationWorkflowManager from "../authentication-workflow/authenticationWorkflowManager"; + ``` + + +```js +import AgoraManager from "../agora_manager/agora_manager.js"; +import AgoraRTC from "agora-rtc-sdk-ng"; +``` + diff --git a/assets/code/video-sdk/live-streaming-multiple-channels/join-a-second-channel.mdx b/assets/code/video-sdk/live-streaming-multiple-channels/join-a-second-channel.mdx new file mode 100644 index 000000000..803eeda53 --- /dev/null +++ b/assets/code/video-sdk/live-streaming-multiple-channels/join-a-second-channel.mdx @@ -0,0 +1,236 @@ + + ```kotlin + fun joinSecondChannel() { + // Create an RtcEngineEx instance + // This interface class contains multi-channel methods + agoraEngineEx = RtcEngineEx.create(mContext, appId, secondChannelEventHandler) as RtcEngineEx + // By default, the video module is disabled, call enableVideo to enable it. + agoraEngineEx.enableVideo() + + if (isSecondChannelJoined) { + agoraEngineEx.leaveChannelEx(rtcSecondConnection) + } else { + val mediaOptions = ChannelMediaOptions() + if (!isBroadcaster) { // Audience Role + mediaOptions.autoSubscribeAudio = true + mediaOptions.autoSubscribeVideo = true + mediaOptions.clientRoleType = Constants.CLIENT_ROLE_AUDIENCE + } else { // Host Role + mediaOptions.publishCameraTrack = true + mediaOptions.publishMicrophoneTrack = true + mediaOptions.channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING + mediaOptions.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER + } + rtcSecondConnection = RtcConnection() + rtcSecondConnection!!.channelId = secondChannelName + rtcSecondConnection!!.localUid = secondChannelUid + + if (isValidURL(serverUrl)) { + fetchToken(secondChannelName, secondChannelUid, object : TokenCallback { + override fun onTokenReceived(rtcToken: String?) { + // Handle the received rtcToken + if (rtcToken != null) secondChannelToken = rtcToken + agoraEngineEx.joinChannelEx( + secondChannelToken, + rtcSecondConnection, + mediaOptions, + secondChannelEventHandler + ) + } + + override fun onError(errorMessage: String) { + // Handle the error + sendMessage("Error: $errorMessage") + } + }) + } else { + agoraEngineEx.joinChannelEx( + secondChannelToken, + rtcSecondConnection, + mediaOptions, + secondChannelEventHandler + ) + } + } + } + ``` + - RtcEngineEx + - RtcConnection + - joinChannelEx + - leaveChannelEx + + + + `joinChannelEx` method lets you join a second channel. If you've been already joined a second channel, `leaveChannelEx` can let to leave that channel. + + ```swift + func joinChannelEx(token: String?) -> Int32 { + let mediaOptions = AgoraRtcChannelMediaOptions() + mediaOptions.channelProfile = .liveBroadcasting + mediaOptions.clientRoleType = .audience + mediaOptions.autoSubscribeAudio = true + mediaOptions.autoSubscribeVideo = true + + return agoraEngine.joinChannelEx( + byToken: token, connection: self.secondConnection, + delegate: nil, mediaOptions: mediaOptions + ) + } + ``` + + Take a look at [`ChannelRelayView`](https://github.com/AgoraIO/video-sdk-samples-ios/blob/main/live-streaming-over-multiple-channels/ChannelRelayView.swift) for an example on how to use `joinChannelEx`, `leaveChannelEx` methods to implement this behavior. + + + - joinChannelEx(byToken:connection:delegate:mediaOptions:joinSuccess:) + + + - joinChannelEx(byToken:connection:delegate:mediaOptions:joinSuccess:) + + + + + ```csharp + // Method to join the second channel. + public void JoinSecondChannel() + { + if (agoraEngineEx != null) + { + if (string.IsNullOrEmpty(configData.secondChannelToken) || string.IsNullOrEmpty(configData.secondChannelName)) + { + Debug.Log("please specify a valid channel name and a token for the second channel"); + return; + } + ChannelMediaOptions mediaOptions = new ChannelMediaOptions(); + mediaOptions.autoSubscribeAudio.SetValue(true); + mediaOptions.autoSubscribeVideo.SetValue(true); + mediaOptions.publishCameraTrack.SetValue(true); + mediaOptions.publishMicrophoneTrack.SetValue(true); + mediaOptions.clientRoleType.SetValue(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER); + mediaOptions.channelProfile.SetValue(CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING); + rtcSecondConnection = new RtcConnection(); + rtcSecondConnection.channelId = configData.secondChannelName; + rtcSecondConnection.localUid = configData.secondChannelUID; + agoraEngineEx.JoinChannelEx(configData.secondChannelToken, rtcSecondConnection, mediaOptions); + } + else + { + Debug.Log("Engine was not initialized"); + } + } + ``` + - ChannelMediaOptions + - RtcConnection + - JoinChannelEx + + + + ```typescript + const JoinSecondChannel = ({ agoraEngineSubscriber }: { agoraEngineSubscriber: IAgoraRTCClient }) => { + const [joinSecondChannelVisible, setJoinSecondChannelVisible] = useState(false); + const remoteUsers = useRemoteUsers(); + const { isLoading: isLoadingCam, localCameraTrack } = useLocalCameraTrack(joinSecondChannelVisible); + const { isLoading: isLoadingMic, localMicrophoneTrack } = useLocalMicrophoneTrack(joinSecondChannelVisible); + + const connection = useConnectionState(agoraEngineSubscriber); + useJoin({ + appid: config.appId, + channel: config.secondChannel, + token: config.secondChannelToken, + uid: config.secondChannelUID, + }, joinSecondChannelVisible, agoraEngineSubscriber); + + usePublish([localMicrophoneTrack, localCameraTrack], (connection == "CONNECTED") && joinSecondChannelVisible , agoraEngineSubscriber); + const handleButtonClick = () => { + setJoinSecondChannelVisible((prev) => !prev); + // You can perform any other logic here if needed + }; + return( +
+ + {remoteUsers.map((remoteUser) => ( +
+ + +
+ ))} +
+ ) + } + ``` + - useLocalCameraTrack + - useLocalMicrophoneTrack + - useConnectionState + - usePublish + - useJoin + - useRemoteUsers +
+ +```js +const handleMultipleChannels = async (isMultipleChannel, clientRole, secondChannelName, secondChannelToken, channelParameters) => { + if (isMultipleChannel == false) { + // Create an Agora engine instance. + agoraEngineSubscriber = AgoraRTC.createClient({ + mode: "live", + codec: "vp9", + }); + // Setup event handlers to subscribe and unsubscribe to the second channel users. + agoraEngineSubscriber.on("user-published", async (user, mediaType) => { + // Subscribe to the remote user when the SDK triggers the "user-published" event. + await agoraEngineSubscriber.subscribe(user, mediaType); + console.log("Subscribe success!"); + if (clientRole == "") { + // set role to broadcaster + await agoraEngineSubscriber.setClientRole(ClientRoleType.Broadcaster); + } + // You only play the video when you join the channel as a host. + else if (clientRole == "audience" && mediaType == "video") { + // Dynamically create a container in the form of a DIV element to play the second channel remote video track. + const container = document.createElement("div"); + // Set the container size. + container.style.width = "640px"; + container.style.height = "480px"; + container.style.padding = "15px 5px 5px 5px"; + // Specify the container id and text. + container.id = user.uid.toString(); + container.textContent = + "Remote user from the second channel" + user.uid.toString(); + // Append the container to page body. + document.body.append(container); + // Play the remote video in the container. + user.videoTrack.play(container); + } + // Listen for the "user-unpublished" event. + agoraEngineSubscriber.on("user-unpublished", (user) => { + console.log(user.uid + "has left the channel"); + }); + }); + // Set the user role. + agoraEngineSubscriber.setClientRole(clientRole); + // Join the new channel. + await agoraEngineSubscriber.join( + config.appId, + secondChannelName, + secondChannelToken, + config.uid + ); + // An audience can not publish audio and video tracks in the channel. + if (clientRole != "audience") { + await agoraEngineSubscriber.publish([ + channelParameters.localAudioTrack, + channelParameters.localVideoTrack, + ]); + } + isMultipleChannel = true; + // Update the button text. + document.getElementById("multiple-channels").innerHTML = + "Leave Second Channel"; + } else { + isMultipleChannel = false; + // Leave the channel. + await agoraEngineSubscriber.leave(); + } +}; +``` + diff --git a/assets/code/video-sdk/live-streaming-multiple-channels/leave-second-channel.mdx b/assets/code/video-sdk/live-streaming-multiple-channels/leave-second-channel.mdx new file mode 100644 index 000000000..ed8c23d28 --- /dev/null +++ b/assets/code/video-sdk/live-streaming-multiple-channels/leave-second-channel.mdx @@ -0,0 +1,35 @@ + + + ```kotlin + agoraEngineEx.leaveChannelEx(rtcSecondConnection) + ``` + - leaveChannelEx + + + ```csharp + // Method to leave the second channel. + public void LeaveSecondChannel() + { + if (agoraEngineEx != null) + { + agoraEngineEx.LeaveChannelEx(rtcSecondConnection); + } + } + ``` + - LeaveChannelEx + + + + ```swift + func leaveChannelEx() -> Int32 { + agoraEngine.leaveChannelEx(self.secondConnection, leaveChannelBlock: nil) + } + ``` + + + - leaveChannelEx(_:leaveChannelBlock:) + + + - leaveChannelEx(_:leaveChannelBlock:) + + \ No newline at end of file diff --git a/assets/code/video-sdk/live-streaming-multiple-channels/monitor-channel-media-relay-state.mdx b/assets/code/video-sdk/live-streaming-multiple-channels/monitor-channel-media-relay-state.mdx new file mode 100644 index 000000000..0dc316afd --- /dev/null +++ b/assets/code/video-sdk/live-streaming-multiple-channels/monitor-channel-media-relay-state.mdx @@ -0,0 +1,111 @@ + + ```kotlin + override val iRtcEngineEventHandler: IRtcEngineEventHandler + get() = object : IRtcEngineEventHandler() { + override fun onChannelMediaRelayStateChanged(state: Int, code: Int) { + if (state == 2) { + mediaRelaying = true + } else if (state == 3) { + mediaRelaying = false + } + } + } + ``` + - onChannelMediaRelayStateChanged + + + + Use the following callback to have your respond to connection and failure events: + + ```swift + func rtcEngine( + _ engine: AgoraRtcEngineKit, + channelMediaRelayStateDidChange state: AgoraChannelMediaRelayState, + error: AgoraChannelMediaRelayError + ) { + switch state { + case .connecting: + // Channel media relay is connecting. + break + case .running: + // Channel media relay is running. + break + case .failure: + // Channel media relay failure + break + default: return + } + } + ``` + + + - rtcEngine(_:channelMediaRelayStateDidChange:error:) + + + - rtcEngine(_:channelMediaRelayStateDidChange:error:) + + + + + ```csharp + // Event handler class to handle the events raised by Agora's RtcEngine instance + internal class MultiChannelLiveStreamingEventHandler : UserEventHandler + { + private MultiChannelLiveStreamingManager multiChannelLiveStreamingManager; + + internal MultiChannelLiveStreamingEventHandler(MultiChannelLiveStreamingManager videoSample) : base(videoSample) + { + multiChannelLiveStreamingManager = videoSample; + } + + public override void OnChannelMediaRelayStateChanged(int state, int code) + { + // This example shows messages in the debug console when the relay state changes, + // a production level app needs to handle state change properly. + switch (state) + { + case 1: // RELAY_STATE_CONNECTING: + Debug.Log("Channel media relay connecting."); + break; + case 2: // RELAY_STATE_RUNNING: + Debug.Log("Channel media relay running."); + break; + case 3: // RELAY_STATE_FAILURE: + Debug.Log("Channel media relay failure. Error code: " + code); + break; + } + } + } + ``` + - OnChannelMediaRelayStateChanged + + +```typescript +const useChannelMediaRelayState = () => { + const agoraEngine = useRTCClient(); + useClientEvent(agoraEngine, "channel-media-relay-state", (state: ChannelMediaRelayState, code: ChannelMediaRelayError) => { + console.log("Channel media relay state changed :", state); + if(code) + { + console.error("Channel media relay error :", code); + } + }); +}; + +const useChannelMediaRelayEvent = () => { + const agoraEngine = useRTCClient(); + useClientEvent(agoraEngine, "channel-media-relay-event", (event: ChannelMediaRelayEvent) => { + console.log("Channel media relay event :", event); + }) +}; +``` +- useClientEvent + + +```js +agoraEngine.on("channel-media-relay-state", state => +{ + console.log("The current state is : "+ state); +}); +``` + diff --git a/assets/code/video-sdk/live-streaming-multiple-channels/receive-callbacks-from-second-channel.mdx b/assets/code/video-sdk/live-streaming-multiple-channels/receive-callbacks-from-second-channel.mdx new file mode 100644 index 000000000..49a06425f --- /dev/null +++ b/assets/code/video-sdk/live-streaming-multiple-channels/receive-callbacks-from-second-channel.mdx @@ -0,0 +1,94 @@ + + ```kotlin + // Callbacks for the second channel + private val secondChannelEventHandler: IRtcEngineEventHandler = + object : IRtcEngineEventHandler() { + override fun onJoinChannelSuccess(channel: String, uid: Int, elapsed: Int) { + isSecondChannelJoined = true + sendMessage("Joined channel $secondChannelName, uid: $uid") + val eventArgs = mutableMapOf() + eventArgs["channel"] = channel + eventArgs["uid"] = uid + mListener?.onEngineEvent("onJoinChannelSuccess2", eventArgs) + } + + override fun onLeaveChannel(stats: RtcStats) { + isSecondChannelJoined = false + sendMessage("Left the channel $secondChannelName") + val eventArgs = mutableMapOf() + eventArgs["stats"] = stats + mListener?.onEngineEvent("onLeaveChannel2", eventArgs) + } + + override fun onUserJoined(uid: Int, elapsed: Int) { + sendMessage(String.format("user %d joined!", uid)) + + // Create surfaceView for remote video + val remoteSurfaceView = SurfaceView(mContext) + remoteSurfaceView.setZOrderMediaOverlay(true) + + // Setup remote video to render + agoraEngineEx.setupRemoteVideoEx( + VideoCanvas( + remoteSurfaceView, + VideoCanvas.RENDER_MODE_HIDDEN, uid + ), rtcSecondConnection + ) + + val eventArgs = mutableMapOf() + eventArgs["uid"] = uid + eventArgs["surfaceView"] = remoteSurfaceView + mListener?.onEngineEvent("onUserJoined2", eventArgs) + } + + override fun onUserOffline(uid: Int, reason: Int) { + val eventArgs = mutableMapOf() + eventArgs["uid"] = uid + mListener?.onEngineEvent("onUserOffline2", eventArgs) + } + } + } + ``` + - IRtcEngineEventHandler + - setupRemoteVideoEx + + + + Use the following callbacks to receive streams and state change notifications of the secondary channel, with a separate delegate: + + ```swift + public class ExDelegate: NSObject, AgoraRtcEngineDelegate { + + let connection: AgoraRtcConnection + + // Catch remote streams from the secondary channel + public func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + // remote user joined channel + } + // Catch when the local user leaves the secondary channel + public func rtcEngine(_ engine: AgoraRtcEngineKit, didLeaveChannelWith stats: AgoraChannelStats) { + // local user left channel + } + // Catch remote streams ended/left from the secondary channel + public func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + // remote user left channel, or connection lost + } + } + ``` + + You will need to set the delegate in `joinChannelEx`. + + + - AgoraRtcEngineDelegate + - rtcEngine(_:didJoinedOfUid:elapsed:) + - rtcEngine(_:didLeaveChannelWith:) + - rtcEngine(_:didOfflineOfUid:reason:) + + + - AgoraRtcEngineDelegate + - rtcEngine(_:didJoinedOfUid:elapsed:) + - rtcEngine(_:didLeaveChannelWith:) + - rtcEngine(_:didOfflineOfUid:reason:) + + + \ No newline at end of file diff --git a/assets/code/video-sdk/live-streaming-multiple-channels/set-variables.mdx b/assets/code/video-sdk/live-streaming-multiple-channels/set-variables.mdx new file mode 100644 index 000000000..1028d2dfb --- /dev/null +++ b/assets/code/video-sdk/live-streaming-multiple-channels/set-variables.mdx @@ -0,0 +1,72 @@ + + ```kotlin + // Channel media relay variables + private var destinationChannelName: String // Name of the destination channel + private var destinationChannelToken: String // Access token for the destination channel + private var destinationChannelUid = 0 // User ID that the user uses in the destination channel + private var sourceChannelToken: String // Access token for the source channel, Generate using channelName and uid = 0 + private var mediaRelaying = false + + // Multi channel streaming variables + private lateinit var agoraEngineEx: RtcEngineEx + private var rtcSecondConnection: RtcConnection? = null + private var secondChannelName: String // Name of the second channel" + private var secondChannelUid = 0 // Uid for the second channel + private var secondChannelToken: String // Access token for the second channel + private var isSecondChannelJoined = false // Track connection state of the second channel + ``` + + + ```swift + // channel id for the primary channel + var primaryChannel: String + // channel id for the secondary channel + var secondaryChannel: String + // Can be any number, the range is arbitrary + var destUid: UInt = .random(in: 1000...5000) + + /// AgoraRtcConnection object for joining and leaving secondary channels + var secondConnection: AgoraRtcConnection { + AgoraRtcConnection( + channelId: self.secondaryChannel, + localUid: Int(self.destUid) + ) + } + ``` + + + - AgoraRtcConnection + + + - AgoraRtcConnection + + + + + ```csharp + private RtcConnection rtcSecondConnection; + internal IRtcEngineEx agoraEngineEx; + ``` + + +```js +// A variable to track the co-hosting state. +var isCoHost = false; +// The destination channel name you want to join. +var destChannelName = ""; +//In a production app, the user adds the channel name and you retrieve the +// authentication token from a token server. +var destChannelToken = ""; +// A variable to track the multiple channel state. +var isMultipleChannel = false; +// Local user role. +var clientRole = "host"; +// The second channel name you want to join. +var secondChannelName = ""; +//In a production app, the user adds the channel name and you retrieve the +// authentication token from a token server. +var secondChannelToken = ""; +// A variable to create a second instance of Agora engine. +var agoraEngineSubscriber; +``` + diff --git a/assets/code/video-sdk/live-streaming-multiple-channels/start-stop-channel-media-relay.mdx b/assets/code/video-sdk/live-streaming-multiple-channels/start-stop-channel-media-relay.mdx new file mode 100644 index 000000000..fbd78fd5b --- /dev/null +++ b/assets/code/video-sdk/live-streaming-multiple-channels/start-stop-channel-media-relay.mdx @@ -0,0 +1,272 @@ + + ```kotlin + fun channelRelay() { + if (agoraEngine == null) { + return + } + + if (mediaRelaying) { + agoraEngine!!.stopChannelMediaRelay() + } else { + // Configure the source channel information + val srcChannelInfo = ChannelMediaInfo(channelName, sourceChannelToken, 0) + val mediaRelayConfiguration = ChannelMediaRelayConfiguration() + mediaRelayConfiguration.setSrcChannelInfo(srcChannelInfo) + + // Configure the destination channel information. + val destChannelInfo = ChannelMediaInfo(destinationChannelName, destinationChannelToken, destinationChannelUid) + mediaRelayConfiguration.setDestChannelInfo(destinationChannelName, destChannelInfo) + + // Start relaying media streams across channels + agoraEngine!!.startOrUpdateChannelMediaRelay(mediaRelayConfiguration) + } + } + ``` + - ChannelMediaRelayConfiguration + - startOrUpdateChannelMediaRelay + - stopChannelMediaRelay + - pauseAllChannelMediaRelay + - resumeAllChannelMediaRelay + + + + You can use `startOrUpdateChannelMediaRelay`, `stopChannelMediaRelay` methods to manage the state of relaying media streams across channels. + + ```swift + func setupMediaRelay( + sourceToken: String?, destinationToken: String? + ) -> Int32 { + // Configure the source channel information. + let srcChannelInfo = AgoraChannelMediaRelayInfo(token: sourceToken) + srcChannelInfo.channelName = self.primaryChannel + srcChannelInfo.uid = 0 + let mediaRelayConfiguration = AgoraChannelMediaRelayConfiguration() + mediaRelayConfiguration.sourceInfo = srcChannelInfo + + // Configure the destination channel information. + let destChannelInfo = AgoraChannelMediaRelayInfo(token: destinationToken) + destChannelInfo.channelName = self.secondaryChannel + destChannelInfo.uid = self.destUid + mediaRelayConfiguration.setDestinationInfo( + destChannelInfo, forChannelName: self.secondaryChannel + ) + + // Start relaying media streams across channels + return agoraEngine.startOrUpdateChannelMediaRelay(mediaRelayConfiguration) + } + + func stopMediaRelay() -> Int32 { + agoraEngine.stopChannelMediaRelay() + } + ``` + + + - startOrUpdateChannelMediaRelay(_:) + - stopChannelMediaRelay() + + Have a look at [`ChannelRelayView`](https://github.com/AgoraIO/video-sdk-samples-ios/blob/main/live-streaming-over-multiple-channels/ChannelRelayView.swift) for details on how to implement a relay media toggle using `startOrUpdateChannelMediaRelay`, `stopChannelMediaRelay` methods. + + + - startOrUpdateChannelMediaRelay(_:) + - stopChannelMediaRelay() + + Have a look at [`ChannelRelayView`](https://github.com/AgoraIO/video-sdk-samples-macos/blob/main/live-streaming-over-multiple-channels/ChannelRelayView.swift) for details on how to implement a relay media toggle using `startOrUpdateChannelMediaRelay`, `stopChannelMediaRelay` methods. + + + + + ```csharp + // Method to relay media to the destination channel. + public void StartChannelRelay() + { + if (agoraEngine != null) + { + if (string.IsNullOrEmpty(configData.destChannelName) || string.IsNullOrEmpty(configData.destToken)) + { + Debug.Log("Specify a valid destination channel name and token."); + return; + } + + // Configure a ChannelMediaRelayConfiguration instance to add source and destination channels. + ChannelMediaRelayConfiguration mediaRelayConfiguration = new ChannelMediaRelayConfiguration(); + + // Configure the source channel information. + mediaRelayConfiguration.srcInfo = new ChannelMediaInfo + { + channelName = configData.channelName, + uid = configData.uid, + token = configData.rtcToken + }; + + // Set up the destination channel information. + mediaRelayConfiguration.destInfos = new ChannelMediaInfo[1]; + mediaRelayConfiguration.destInfos[0] = new ChannelMediaInfo + { + channelName = configData.destChannelName, + uid = configData.destUID, + token = configData.destToken + }; + + // Number of destination channels. + mediaRelayConfiguration.destCount = 1; + + // Start media relaying + agoraEngine.StartOrUpdateChannelMediaRelay(mediaRelayConfiguration); + } + else + { + Debug.Log("Agora Engine is not initialized. Click 'Join' to join the primary channel and then join the second channel."); + } + } + + // Method to stop media relaying. + public void StopChannelRelay() + { + if (agoraEngine != null) + { + agoraEngine.StopChannelMediaRelay(); + } + } + ``` + - ChannelMediaRelayConfiguration + - StartOrUpdateChannelMediaRelay + - StopChannelMediaRelay + + + + ```typescript + const ChannelMediaRelay = () => { + const agoraEngine = useRTCClient(); + const channelMediaConfig = AgoraRTC.createChannelMediaRelayConfiguration(); + const [isRelayRunning, setIsRelayRunning] = useState(false); + const connectionState = useConnectionState(); + + // Channel media relay events. + useChannelMediaRelayState(); + useChannelMediaRelayEvent(); + + if(config.destChannelName === "" || config.destChannelToken === "") + { + console.log("Please specify a valid channel name and a valid token for the destination channel in the config file"); + return; + } + channelMediaConfig.setSrcChannelInfo({ + channelName: config.channelName, + token: config.rtcToken, + uid: 0, + }); + channelMediaConfig.addDestChannelInfo({ + channelName: config.destChannelName, + token: config.destChannelToken, + uid: config.destUID, + }); + + const startChannelMediaRelay = () => { + agoraEngine + .startChannelMediaRelay(channelMediaConfig) + .then(() => { + console.log("Channel relay started successfully"); + setIsRelayRunning(true); + }) + .catch((e) => { + console.log(`startChannelMediaRelay failed`, e); + }); + }; + + const stopChannelMediaRelay = () => { + agoraEngine.stopChannelMediaRelay() + .then(() => { + console.log("Channel relay stopped successfully"); + setIsRelayRunning(false); + }) + .catch((e) => { + console.log(`stopChannelMediaRelay failed`, e); + }); + }; + + return ( +
+ +
+ ); + }; + ``` + - createChannelMediaRelayConfiguration + - startChannelMediaRelay + - stopChannelMediaRelay + - updateChannelMediaRelay + +
+ +```js +const handleChannelMediaRelay = ( + isCoHost, + destUID, + destChannelName, + destChannelToken +) => { + const channelMediaConfig = AgoraRTC.createChannelMediaRelayConfiguration(); + if (!isCoHost) { + // Set the source channel information. + // Set channelName as the source channel name. Set uid as the ID of the host whose stream is relayed. + // The token is generated with the source channel name. + // Assign the token you generated for the source channel. + console.log("entering handleChannelMediaRelay"); + channelMediaConfig.setSrcChannelInfo({ + channelName: config.channelName, + token: config.token, + uid: config.uid, + }); + // Set the destination channel information. You can set a maximum of four destination channels. + // Set channelName as the destination channel name. Set uid as 0 or a 32-bit unsigned integer. + // To avoid UID conflicts, the uid must be different from any other user IDs in the destination channel. + // Assign the token you generated for the destination channel. + channelMediaConfig.addDestChannelInfo({ + channelName: destChannelName, + token: destChannelToken, + uid: destUID, + }); + // Start media relaying. + agoraManager + .getAgoraEngine() + .startChannelMediaRelay(channelMediaConfig) + .then(() => { + // Update the button text. + document.getElementById(`coHost`).innerHTML = + "Stop Channel Media Relay"; + console.log(`startChannelMediaRelay success`); + }) + .catch((e) => { + console.log(`startChannelMediaRelay failed`, e); + }); + } else { + // Remove a destination channel. + channelMediaConfig.removeDestChannelInfo(destChannelName); + // Update the configurations of the media stream relay. + agoraManager + .getAgoraEngine() + .updateChannelMediaRelay(channelMediaConfig) + .then(() => { + console.log("updateChannelMediaRelay success"); + }) + .catch((e) => { + console.log("updateChannelMediaRelay failed", e); + }); + //Stop the relay. + agoraManager + .getAgoraEngine() + .stopChannelMediaRelay() + .then(() => { + console.log("stop media relay success"); + }) + .catch((e) => { + console.log("stop media relay failed", e); + }); + // Update the button text. + document.getElementById(`coHost`).innerHTML = "Start Channel Media Relay"; + } +}; +``` + diff --git a/assets/code/video-sdk/live-streaming-multiple-channels/swift/configure-buttons.mdx b/assets/code/video-sdk/live-streaming-multiple-channels/swift/configure-buttons.mdx index 1a4bd9b0c..0e9653c79 100644 --- a/assets/code/video-sdk/live-streaming-multiple-channels/swift/configure-buttons.mdx +++ b/assets/code/video-sdk/live-streaming-multiple-channels/swift/configure-buttons.mdx @@ -1,5 +1,5 @@ -``` swift +```swift channelRelayBtn = NSButton() channelRelayBtn.frame = CGRect(x: 255, y: 10, width: 150, height: 20) channelRelayBtn.title = "Start Channel Media Relay" @@ -9,7 +9,7 @@ self.view.addSubview(channelRelayBtn) ``` -``` swift +```swift channelRelayBtn = UIButton(type: .system) channelRelayBtn.frame = CGRect(x: 100, y: 550, width: 200, height: 50) channelRelayBtn.setTitle("Start Channel Media Relay", for: .normal) diff --git a/assets/code/video-sdk/live-streaming-multiple-channels/swift/create-ui.mdx b/assets/code/video-sdk/live-streaming-multiple-channels/swift/create-ui.mdx index 10ecc34c8..adaa456fc 100644 --- a/assets/code/video-sdk/live-streaming-multiple-channels/swift/create-ui.mdx +++ b/assets/code/video-sdk/live-streaming-multiple-channels/swift/create-ui.mdx @@ -1,10 +1,10 @@ -``` swift +```swift var channelRelayBtn: NSButton! ``` -``` swift +```swift var channelRelayBtn: UIButton! ``` \ No newline at end of file diff --git a/assets/code/video-sdk/live-streaming-multiple-channels/swift/mc-configure-buttons.mdx b/assets/code/video-sdk/live-streaming-multiple-channels/swift/mc-configure-buttons.mdx index cb3d7e4ad..c80e99325 100644 --- a/assets/code/video-sdk/live-streaming-multiple-channels/swift/mc-configure-buttons.mdx +++ b/assets/code/video-sdk/live-streaming-multiple-channels/swift/mc-configure-buttons.mdx @@ -1,5 +1,5 @@ -``` swift +```swift secondChannelBtn = NSButton() secondChannelBtn.frame = CGRect(x: 255, y: 40, width: 180, height: 50) secondChannelBtn.title = "Join second channel" @@ -9,7 +9,7 @@ self.view.addSubview(secondChannelBtn) ``` -``` swift +```swift secondChannelBtn = UIButton(type: .system) secondChannelBtn.frame = CGRect(x: 120, y: 600, width: 180, height: 50) secondChannelBtn.setTitle("Join second channel", for: .normal) diff --git a/assets/code/video-sdk/live-streaming-multiple-channels/swift/mc-create-ui.mdx b/assets/code/video-sdk/live-streaming-multiple-channels/swift/mc-create-ui.mdx index c3c14e352..9d6511316 100644 --- a/assets/code/video-sdk/live-streaming-multiple-channels/swift/mc-create-ui.mdx +++ b/assets/code/video-sdk/live-streaming-multiple-channels/swift/mc-create-ui.mdx @@ -1,10 +1,10 @@ -``` swift +```swift var secondChannelBtn: NSButton! ``` -``` swift +```swift var secondChannelBtn: UIButton! ``` \ No newline at end of file diff --git a/assets/code/video-sdk/live-streaming-multiple-channels/swift/mc-join-second-channel.mdx b/assets/code/video-sdk/live-streaming-multiple-channels/swift/mc-join-second-channel.mdx index 1c2a95ade..cce82c2ad 100644 --- a/assets/code/video-sdk/live-streaming-multiple-channels/swift/mc-join-second-channel.mdx +++ b/assets/code/video-sdk/live-streaming-multiple-channels/swift/mc-join-second-channel.mdx @@ -1,5 +1,5 @@ -``` swift +```swift @objc func secondChannelBtnClicked() { if isSecondChannelJoined { let result = agoraEngine.leaveChannelEx(rtcSecondConnection, leaveChannelBlock: nil) @@ -41,7 +41,7 @@ ``` -``` swift +```swift @objc func secondChannelBtnClicked() { if isSecondChannelJoined { let result = agoraEngine.leaveChannelEx(rtcSecondConnection, leaveChannelBlock: nil) diff --git a/assets/code/video-sdk/live-streaming-multiple-channels/swift/mc-second-channel-delegate.mdx b/assets/code/video-sdk/live-streaming-multiple-channels/swift/mc-second-channel-delegate.mdx index 120f73df0..7e1f3714e 100644 --- a/assets/code/video-sdk/live-streaming-multiple-channels/swift/mc-second-channel-delegate.mdx +++ b/assets/code/video-sdk/live-streaming-multiple-channels/swift/mc-second-channel-delegate.mdx @@ -1,5 +1,5 @@ -``` swift +```swift class SecondChannelDelegate: NSObject, AgoraRtcEngineDelegate { func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { print("Join Channel Success - joined channel: \(channel) uid: \(uid)") @@ -19,7 +19,7 @@ class SecondChannelDelegate: NSObject, AgoraRtcEngineDelegate { ``` -``` swift +```swift class SecondChannelDelegate: NSObject, AgoraRtcEngineDelegate { func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { print("Join Channel Success - joined channel: \(channel) uid: \(uid)") diff --git a/assets/code/video-sdk/live-streaming-multiple-channels/swift/monitor-relay-state.mdx b/assets/code/video-sdk/live-streaming-multiple-channels/swift/monitor-relay-state.mdx index 2dab6d3bb..0371c5c6c 100644 --- a/assets/code/video-sdk/live-streaming-multiple-channels/swift/monitor-relay-state.mdx +++ b/assets/code/video-sdk/live-streaming-multiple-channels/swift/monitor-relay-state.mdx @@ -1,5 +1,5 @@ -``` swift +```swift func rtcEngine(_ engine: AgoraRtcEngineKit, channelMediaRelayStateDidChange state: AgoraChannelMediaRelayState, error: AgoraChannelMediaRelayError) { // This example shows toast messages when the relay state changes, @@ -34,7 +34,7 @@ func rtcEngine(_ engine: AgoraRtcEngineKit, ``` - ``` swift + ```swift func rtcEngine(_ engine: AgoraRtcEngineKit, channelMediaRelayStateDidChange state: AgoraChannelMediaRelayState, error: AgoraChannelMediaRelayError) { // This example shows toast messages when the relay state changes, diff --git a/assets/code/video-sdk/play-media/configure-engine.mdx b/assets/code/video-sdk/play-media/configure-engine.mdx new file mode 100644 index 000000000..a3d694669 --- /dev/null +++ b/assets/code/video-sdk/play-media/configure-engine.mdx @@ -0,0 +1,21 @@ + +```typescript +function MediaPlaying() { + const agoraEngine = useRTCClient(AgoraRTC.createClient({ codec: "vp8", mode: config.selectedProduct })); + + return ( +
+

Stream media to a channel

+ + + + + +
+ ); +} +``` + - useRTCClient + - AgoraRTCProvider + +
\ No newline at end of file diff --git a/assets/code/video-sdk/play-media/destroy-media-player.mdx b/assets/code/video-sdk/play-media/destroy-media-player.mdx new file mode 100644 index 000000000..d6a87d34b --- /dev/null +++ b/assets/code/video-sdk/play-media/destroy-media-player.mdx @@ -0,0 +1,52 @@ + + ```kotlin + fun destroyMediaPlayer(){ + // Destroy the media player instance and clean up + if (mediaPlayer != null) { + mediaPlayer?.stop() + mediaPlayer?.unRegisterPlayerObserver(mediaPlayerObserver) + mediaPlayer?.destroy() + mediaPlayer = null + } + } + ``` + - stop + - unregisterPlayerSourceObserver + - destroyMediaPlayer + + + ```csharp + public void DestroyMediaPlayer() + { + mediaPlayer.Dispose(); // Dispose of the media player instance + mediaPlayer = null; // Set the media player reference to null + } + + public override void DestroyEngine() + { + // Destroy the media player + if (mediaPlayer != null) + { + mediaPlayer.Stop(); + DestroyMediaPlayer(); + } + + base.DestroyEngine(); // Call the base class's engine cleanup method + } + ``` + * Dispose + + + + ```swift + agoraEngine.destroyMediaPlayer(mediaPlayer) + ``` + + + - destroyMediaPlayer(_:) + + + - destroyMediaPlayer(_:) + + + \ No newline at end of file diff --git a/assets/code/video-sdk/play-media/display-media.mdx b/assets/code/video-sdk/play-media/display-media.mdx new file mode 100644 index 000000000..0df9b3366 --- /dev/null +++ b/assets/code/video-sdk/play-media/display-media.mdx @@ -0,0 +1,68 @@ + + ```kotlin + fun mediaPlayerSurfaceView(): SurfaceView { + // Sets up and returns a SurfaceView to display the media player output + // Instantiate a SurfaceView + val videoSurfaceView = SurfaceView(mContext) + // Create a VideoCanvas using the SurfaceView + val videoCanvas = VideoCanvas( + videoSurfaceView, + Constants.RENDER_MODE_HIDDEN, + 0 + ) + // Set the source type and media player Id + videoCanvas.sourceType = Constants.VIDEO_SOURCE_MEDIA_PLAYER + videoCanvas.mediaPlayerId = mediaPlayer?.mediaPlayerId ?: 0 + // Setup the SurfaceView + agoraEngine?.setupLocalVideo(videoCanvas) + + return videoSurfaceView + } + ``` + - getMediaPlayerId + - setupLocalVideo + + + ```swift + canvas.sourceType = .mediaPlayer + canvas.mediaPlayerId = mediaPlayer.getMediaPlayerId() + agoraEngine.setupLocalVideo(canvas) + ``` + + + - sourceType + - getMediaPlayerId() + - setupLocalVideo(_:) + + + - sourceType + - getMediaPlayerId() + - setupLocalVideo(_:) + + + + + ```csharp + public void PreviewMediaTrack(bool previewMedia) + { + GameObject localViewGo = LocalView.gameObject; + + // Add a VideoSurface component to the local view game object + LocalView = localViewGo.AddComponent(); + + if (previewMedia) + { + // Setup local view to display the media file. + LocalView.SetForUser((uint)mediaPlayer.GetId(), "", VIDEO_SOURCE_TYPE.VIDEO_SOURCE_MEDIA_PLAYER); + } + else + { + // Setup local view to display the local video. + LocalView.SetForUser(0, "", VIDEO_SOURCE_TYPE.VIDEO_SOURCE_CAMERA_PRIMARY); + } + } + ``` + * VideoSurface + * SetForUser + + \ No newline at end of file diff --git a/assets/code/video-sdk/play-media/event-handler.mdx b/assets/code/video-sdk/play-media/event-handler.mdx new file mode 100644 index 000000000..36a08aa69 --- /dev/null +++ b/assets/code/video-sdk/play-media/event-handler.mdx @@ -0,0 +1,191 @@ + + ```kotlin + private val mediaPlayerObserver: IMediaPlayerObserver = object : IMediaPlayerObserver { + override fun onPlayerStateChanged(state: MediaPlayerState, error: MediaPlayerError) { + // Reports changes in playback state + if (state == MediaPlayerState.PLAYER_STATE_OPEN_COMPLETED) { + // Read media duration for updating play progress + mediaDuration = mediaPlayer?.duration ?: 0 + } + + // Notify the UI + mediaPlayerListener.onPlayerStateChanged(state, error) + } + + override fun onPositionChanged(position: Long) { + if (mediaDuration > 0) { + // Calculate the progress percentage + val result = (position.toFloat() / mediaDuration.toFloat() * 100).toInt() + // Notify the UI of the progress + mediaPlayerListener.onProgress(result) + } + } + + override fun onPlayerEvent(eventCode: MediaPlayerEvent, elapsedTime: Long, message: String) { + // Required to implement IMediaPlayerObserver + } + + override fun onMetaData(type: MediaPlayerMetadataType, data: ByteArray) { + // Occurs when the media metadata is received + } + + override fun onPlayBufferUpdated(playCachedBuffer: Long) { + // Reports the playback duration that the buffered data can support + } + + override fun onPreloadEvent(src: String, event: MediaPlayerPreloadEvent) { + // Reports the events of preloaded media resources + } + + override fun onPlayerSrcInfoChanged(from: SrcInfo, to: SrcInfo) { + // Occurs when the video bitrate of the media resource changes + } + + override fun onPlayerInfoUpdated(info: PlayerUpdatedInfo) { + // Occurs when information related to the media player changes + } + + override fun onAudioVolumeIndication(volume: Int) { + // Reports the volume of the media player + } + + override fun onAgoraCDNTokenWillExpire() { + // Required to implement IMediaPlayerObserver + } + } + ``` + - IMediaPlayerObserver + + +```csharp +// Internal class for handling media player events +internal class PlayMediaEventHandler : IMediaPlayerSourceObserver +{ + private PlayMediaManager playMediaManager; + + internal PlayMediaEventHandler(PlayMediaManager refPlayMedia) + { + playMediaManager = refPlayMedia; + } + + public override void OnPlayerSourceStateChanged(MEDIA_PLAYER_STATE state, MEDIA_PLAYER_ERROR error) + { + Debug.Log(state.ToString()); + playMediaManager.state = state; + + if (state == MEDIA_PLAYER_STATE.PLAYER_STATE_OPEN_COMPLETED) + { + // Media file opened successfully. Get the duration of the file to set up the progress bar. + playMediaManager.mediaPlayer.GetDuration(ref playMediaManager.mediaDuration); + } + else if (state == MEDIA_PLAYER_STATE.PLAYER_STATE_PLAYING) + { + playMediaManager.PreviewMediaTrack(true); + playMediaManager.PublishMediaFile(); + } + else if (state == MEDIA_PLAYER_STATE.PLAYER_STATE_PLAYBACK_ALL_LOOPS_COMPLETED) + { + playMediaManager.PreviewMediaTrack(false); + playMediaManager.UnpublishMediaFile(); + // Clean up + playMediaManager.mediaPlayer.Dispose(); + playMediaManager.mediaPlayer = null; + } + else if (state == MEDIA_PLAYER_STATE.PLAYER_STATE_PAUSED) + { + playMediaManager.PreviewMediaTrack(false); + playMediaManager.UnpublishMediaFile(); + } + else if (state == MEDIA_PLAYER_STATE.PLAYER_STATE_FAILED) + { + Debug.Log("Media player failed :" + error); + } + } + + public override void OnPositionChanged(long position) + { + if (playMediaManager.mediaDuration > 0) + { + // Update the ProgressBar + playMediaManager.position = position; + } + } + + public override void OnPlayerEvent(MEDIA_PLAYER_EVENT eventCode, long elapsedTime, string message) + { + // Required to implement IMediaPlayerObserver + } + + public override void OnMetaData(byte[] type, int length) + { + // Required to implement IMediaPlayerObserver + } + + public override void OnAudioVolumeIndication(int volume) + { + // Required to implement IMediaPlayerObserver + } + + public override void OnPlayBufferUpdated(Int64 playCachedBuffer) + { + // Required to implement IMediaPlayerObserver + } + + public override void OnPlayerInfoUpdated(PlayerUpdatedInfo info) + { + // Required to implement IMediaPlayerObserver + } + + public override void OnPlayerSrcInfoChanged(SrcInfo from, SrcInfo to) + { + // Required to implement IMediaPlayerObserver + } + + public override void OnPreloadEvent(string src, PLAYER_PRELOAD_EVENT @event) + { + // Required to implement IMediaPlayerObserver + } +} +``` +* IMediaPlayerSourceObserver + + + + ```swift + public func AgoraRtcMediaPlayer( + _ playerKit: AgoraRtcMediaPlayerProtocol, + didChangedTo state: AgoraMediaPlayerState, + error: AgoraMediaPlayerError + ) { + switch state { + case .openCompleted: + // Media file opened successfully + // Update the UI, and start playing + case .playBackAllLoopsCompleted: + // Media file finished playing + case .playing: + // Media started playing + default: break + } + } + + public func AgoraRtcMediaPlayer( + _ playerKit: AgoraRtcMediaPlayerProtocol, + didChangedTo position: Int + ) { + // Progress as a percentage + let progress = Float(position) / Float(playerKit.getDuration()) + } + ``` + + + - AgoraRtcMediaPlayerDelegate + - AgoraRtcMediaPlayer(_:didChangedTo:error:) + - AgoraRtcMediaPlayer(_:didChangedTo:) + + + - AgoraRtcMediaPlayerDelegate + - AgoraRtcMediaPlayer(_:didChangedTo:error:) + - AgoraRtcMediaPlayer(_:didChangedTo:) + + diff --git a/assets/code/video-sdk/play-media/import-library.mdx b/assets/code/video-sdk/play-media/import-library.mdx new file mode 100644 index 000000000..342a6c8c9 --- /dev/null +++ b/assets/code/video-sdk/play-media/import-library.mdx @@ -0,0 +1,24 @@ + + ```kotlin + + ``` + + + ```swift + import AgoraRtcKit + ``` + + +```javascript +import AgoraManager from "../agora_manager/agora_manager.js"; +import AgoraRTC from "agora-rtc-sdk-ng"; +``` + + + ```typescript + import { AgoraRTCProvider, useRTCClient, usePublish, useConnectionState } from "agora-rtc-react"; + import AgoraRTC, { IBufferSourceAudioTrack } from "agora-rtc-sdk-ng"; + import AuthenticationWorkflowManager from "../authentication-workflow/authenticationWorkflowManager"; + import config from "../agora-manager/config"; + ``` + diff --git a/assets/code/video-sdk/play-media/play-pause-resume.mdx b/assets/code/video-sdk/play-media/play-pause-resume.mdx new file mode 100644 index 000000000..f98be18b5 --- /dev/null +++ b/assets/code/video-sdk/play-media/play-pause-resume.mdx @@ -0,0 +1,110 @@ + + ```kotlin + fun playMedia() { + // Start publishing the media player video + updateChannelPublishOptions(true) + mediaPlayer?.play() + } + + fun pauseMedia() { + mediaPlayer?.pause() + } + + fun resumeMedia() { + mediaPlayer?.resume() + } + ``` + - play + - pause + - resume + + + ```csharp + public void PauseMediaFile() + { + mediaPlayer.Pause(); // Pause the media playback + } + + public void ResumeMediaFile() + { + mediaPlayer.Resume(); // Resume paused media playback + } + + public void PlayMediaFile() + { + mediaPlayer.Play(); // Start or resume playing the media file + } + ``` + * Play + * Resume + * Pause + + + ```swift + func playMedia() { self.mediaPlayer?.play() } + func pauseMedia() { self.mediaPlayer?.pause() } + func resumeMedia() { self.mediaPlayer?.resume() } + ``` + + + - play() + - pause() + - resume() + + + - play() + - pause() + - resume() + + + +1. Play and publish the audio file: + ```typescript + const PlayAudioFile: React.FC<{ track: IBufferSourceAudioTrack }> = ({ track }) => { + usePublish([track]); + + useEffect(() => { + track.startProcessAudioBuffer(); + track.play(); // to play the track for the local user + return () => { + track.stopProcessAudioBuffer(); + track.stop(); + }; + }, [track]); + + return
Audio file playing
; + }; + ``` + - usePublish + - startProcessAudioBuffer + - play + - stopProcessAudioBuffer + - stop + +2. Process the selected audio file: + ```typescript + const MediaPlayingComponent: React.FC = () => { + const [isMediaPlaying, setMediaPlaying] = useState(false); + const [audioFileTrack, setAudioFileTrack] = useState(null); + const connectionState = useConnectionState(); + + // Event handler for selecting an audio file + const handleFileChange = (event: React.ChangeEvent) => { + if (event.target.files && event.target.files.length > 0) { + const selectedFile = event.target.files[0]; + try + { + AgoraRTC.createBufferSourceAudioTrack({ source: selectedFile }) + .then((track) => {setAudioFileTrack(track)}) + .catch((error) => {console.error(error);}) + } catch (error) { + console.error("Error creating buffer source audio track:", error); + } + } + }; + } + ``` + - useConnectionState + - BufferSourceAudioTrackInitConfig + - createBufferSourceAudioTrack +
\ No newline at end of file diff --git a/assets/code/video-sdk/play-media/set-variables.mdx b/assets/code/video-sdk/play-media/set-variables.mdx new file mode 100644 index 000000000..deb3de495 --- /dev/null +++ b/assets/code/video-sdk/play-media/set-variables.mdx @@ -0,0 +1,39 @@ + + ```kotlin + private var mediaPlayer: IMediaPlayer? = null // Instance of the media player + private lateinit var mediaPlayerListener: MediaPlayerListener + ``` + + + ```swift + var mediaPlayer: AgoraRtcMediaPlayerProtocol? + ``` + + + - AgoraRtcMediaPlayerProtocol + + + - AgoraRtcMediaPlayerProtocol + + + + ```csharp + // Internal variables for managing media playback + internal IMediaPlayer mediaPlayer; // Instance of the media player + internal bool isMediaPlaying = false; + internal long mediaDuration = 0; + internal MEDIA_PLAYER_STATE state; + internal long position; + ``` + + +```javascript +const streamMedia = async () => { + // Create an audio track from a source file + const track = await AgoraRTC.createBufferSourceAudioTrack({ + source: "./sample.mp3", + }); + // ... + }; +``` + diff --git a/assets/code/video-sdk/play-media/start-streaming.mdx b/assets/code/video-sdk/play-media/start-streaming.mdx new file mode 100644 index 000000000..39e1302e6 --- /dev/null +++ b/assets/code/video-sdk/play-media/start-streaming.mdx @@ -0,0 +1,110 @@ + + + 1. Set up a media player + + ```kotlin + fun setupMediaPlayer(listener: MediaPlayerListener){ + if (mediaPlayer == null) { + // Create an instance of the media player + mediaPlayer = agoraEngine?.createMediaPlayer() + // Set the mediaPlayerObserver to receive callbacks + mediaPlayer?.registerPlayerObserver(mediaPlayerObserver) + // A listener to notify the UI + mediaPlayerListener = listener + } + } + ``` + - IMediaPlayer + - createMediaPlayer + - registerPlayerObserver + + + 1. Open a media file + + ```kotlin + fun openMediaFile(mediaLocation: String) { + // Opens the media file at mediaLocation url + // Supports URI files starting with content:// + if (mediaPlayer != null) { + // Open the media file + mediaPlayer?.open(mediaLocation, 0) + } + } + ``` + - open + + To open a media file and configure the playback scenario, call openWithMediaSource. Use selectMultiAudioTrack to select the audio tracks that you want to play on your local device and publish to the channel respectively. + + + + ```swift + func startStreaming(from url: URL) { + mediaPlayer = agoraEngine.createMediaPlayer(with: self) + mediaPlayer!.open(url.path, startPos: 0) + } + ``` + + + - createMediaPlayer(with:) + - AgoraRtcMediaPlayerProtocol + - open(_:startPos:) + + To open a media file and configure the playback scenario, call open(with:). Use selectMultiAudioTrack(_:) to select the audio tracks that you want to play on your local device and publish to the channel respectively. + + + + - createMediaPlayer(with:) + - AgoraRtcMediaPlayerProtocol + - open(_:startPos:) + + To open a media file and configure the playback scenario, call open(with:). Use selectMultiAudioTrack(_:) to select the audio tracks that you want to play on your local device and publish to the channel respectively. + + + + ```csharp + public void InitMediaPlayer() + { + // Check if the engine exists. + if (agoraEngine == null) + { + // Log a message if the Agora engine is not initialized + Debug.Log("Please click `Join` and then `Play Media` to play the video file"); + return; + } + + // Create an instance of the media player + mediaPlayer = agoraEngine.CreateMediaPlayer(); + + // Create an instance of mediaPlayerObserver class + PlayMediaEventHandler mediaPlayerObserver = new PlayMediaEventHandler(this); + + // Set the mediaPlayerObserver to receive callbacks + mediaPlayer.InitEventHandler(mediaPlayerObserver); + + // Open the media file specified in the configuration data + mediaPlayer.Open(configData.videoFileURL, 0); + } + ``` + * IMediaPlayer + * CreateMediaPlayer + * InitEventHandler + * Open + + + + +```javascript +const streamMedia = async () => { + // Create an audio track from a source file + const track = await AgoraRTC.createBufferSourceAudioTrack({ + source: "./sample.mp3", + }); + // Play the track + track.startProcessAudioBuffer({ loop: false }); + track.play(); + }; +``` +- createBufferSourceAudioTrack +- BufferSourceAudioTrackInitConfig + + diff --git a/assets/code/video-sdk/play-media/swift/configure-buttons.mdx b/assets/code/video-sdk/play-media/swift/configure-buttons.mdx index c1298a7b6..19aeaba14 100644 --- a/assets/code/video-sdk/play-media/swift/configure-buttons.mdx +++ b/assets/code/video-sdk/play-media/swift/configure-buttons.mdx @@ -1,6 +1,6 @@ -``` swift +```swift mediaPlayerBtn = NSButton() mediaPlayerBtn.frame = CGRect(x: 300, y: 60, width: 150, height: 200) mediaPlayerBtn.title = "Open Media File" @@ -15,7 +15,7 @@ self.view.addSubview(mediaProgressView) ``` -``` swift +```swift mediaPlayerBtn = UIButton(type: .system) mediaPlayerBtn.frame = CGRect(x: 100, y:550, width:200, height:50) mediaPlayerBtn.setTitle("Open Media File", for: .normal) diff --git a/assets/code/video-sdk/play-media/swift/create-ui.mdx b/assets/code/video-sdk/play-media/swift/create-ui.mdx index 0dc920b1a..85d6b668b 100644 --- a/assets/code/video-sdk/play-media/swift/create-ui.mdx +++ b/assets/code/video-sdk/play-media/swift/create-ui.mdx @@ -1,12 +1,12 @@ -``` swift +```swift var mediaPlayerBtn: NSButton! var mediaProgressView: NSProgressIndicator! ``` -``` swift +```swift var mediaPlayerBtn: UIButton! var mediaProgressView: UIProgressView! ``` diff --git a/assets/code/video-sdk/play-media/swift/open-play-pause-media.mdx b/assets/code/video-sdk/play-media/swift/open-play-pause-media.mdx index 218523776..244fcd777 100644 --- a/assets/code/video-sdk/play-media/swift/open-play-pause-media.mdx +++ b/assets/code/video-sdk/play-media/swift/open-play-pause-media.mdx @@ -1,6 +1,6 @@ - ``` swift + ```swift @objc func mediaPlayerBtnClicked(sender: NSButton!) { // Initialize the mediaPlayer and open a media file if (mediaPlayer == nil) { @@ -41,7 +41,7 @@ ``` - ``` swift + ```swift @objc func mediaPlayerBtnClicked(sender: UIButton!) { // Initialize the mediaPlayer and open a media file if (mediaPlayer == nil) { diff --git a/assets/code/video-sdk/play-media/update-channel-publish-options.mdx b/assets/code/video-sdk/play-media/update-channel-publish-options.mdx new file mode 100644 index 000000000..89d3d8fb4 --- /dev/null +++ b/assets/code/video-sdk/play-media/update-channel-publish-options.mdx @@ -0,0 +1,79 @@ + + ```kotlin + fun updateChannelPublishOptions(publishMediaPlayer: Boolean) { + val channelOptions = ChannelMediaOptions() + // Start or stop publishing the media player tracks + channelOptions.publishMediaPlayerAudioTrack = publishMediaPlayer + channelOptions.publishMediaPlayerVideoTrack = publishMediaPlayer + // Stop or start publishing the microphone and camera tracks + channelOptions.publishMicrophoneTrack = !publishMediaPlayer + channelOptions.publishCameraTrack = !publishMediaPlayer + // Specify the media player Id for publishing + if (publishMediaPlayer) channelOptions.publishMediaPlayerId = mediaPlayer?.mediaPlayerId + // Implement the settings + agoraEngine?.updateChannelMediaOptions(channelOptions) + } + ``` + - updateChannelMediaOptions + + + ```swift + func updateChannelPublishOptions(publishingMedia: Bool) -> Int32 { + let channelOptions = AgoraRtcChannelMediaOptions() + channelOptions.publishMediaPlayerAudioTrack = publishingMedia + channelOptions.publishMediaPlayerVideoTrack = publishingMedia + // If publishing media player, set the media player ID + if publishingMedia { channelOptions.publishMediaPlayerId = Int(mediaPlayer!.getMediaPlayerId()) } + // Set the regular camera to false if publishing media track + channelOptions.publishMicrophoneTrack = true + channelOptions.publishCameraTrack = !publishingMedia + + return agoraEngine.updateChannel(with: channelOptions) + } + ``` + + + - AgoraRtcChannelMediaOptions + - getMediaPlayerId() + - updateChannel(with:) + + + - AgoraRtcChannelMediaOptions + - getMediaPlayerId() + - updateChannel(with:) + + + + + ```csharp + public void PublishMediaFile() + { + // Configure channel options to publish the media player's audio and video tracks + ChannelMediaOptions channelOptions = new ChannelMediaOptions(); + channelOptions.publishMediaPlayerAudioTrack.SetValue(true); + channelOptions.publishMediaPlayerVideoTrack.SetValue(true); + channelOptions.publishMicrophoneTrack.SetValue(false); + channelOptions.publishCameraTrack.SetValue(false); + channelOptions.publishMediaPlayerId.SetValue(mediaPlayer.GetId()); + + // Update the channel's media options with the configured options + agoraEngine.UpdateChannelMediaOptions(channelOptions); + } + public void UnpublishMediaFile() + { + // Configure channel options to unpublish the media player's audio and video tracks + ChannelMediaOptions channelOptions = new ChannelMediaOptions(); + channelOptions.publishMediaPlayerAudioTrack.SetValue(false); + channelOptions.publishMediaPlayerVideoTrack.SetValue(false); + channelOptions.publishMicrophoneTrack.SetValue(true); + channelOptions.publishCameraTrack.SetValue(true); + + // Update the channel's media options with the configured options + agoraEngine.UpdateChannelMediaOptions(channelOptions); + } + ``` + * ChannelMediaOptions + * GetId + * UpdateChannelMediaOptions + + \ No newline at end of file diff --git a/assets/code/video-sdk/product-workflow/import-library.mdx b/assets/code/video-sdk/product-workflow/import-library.mdx new file mode 100644 index 000000000..b22ceb064 --- /dev/null +++ b/assets/code/video-sdk/product-workflow/import-library.mdx @@ -0,0 +1,52 @@ + + ```kotlin + import io.agora.rtc2.ChannelMediaOptions + import io.agora.rtc2.Constants + import io.agora.rtc2.ScreenCaptureParameters + import io.agora.rtc2.video.VideoCanvas + ``` + + + ```swift + import ReplayKit + import AgoraRtcKit + ``` + + + ```swift + import AgoraRtcKit + ``` + + Make sure to also add the ScreenCapture plugin from the Swift Package. + + + + ```csharp + using Agora.Rtc; + ``` + + +```javascript +import AgoraManager from "../agora_manager/agora_manager.js"; +import AgoraRTC from "agora-rtc-sdk-ng"; +``` + + + ```typescript + import React, { useState, useRef, useEffect } from "react"; + import { + AgoraRTCProvider, + useRTCClient, + useRemoteUsers, + useConnectionState, + useJoin, + usePublish, + useLocalScreenTrack, + useTrackEvent, + } from "agora-rtc-react"; + import AgoraRTC, { DeviceInfo, IAgoraRTCError } from "agora-rtc-sdk-ng"; + import AuthenticationWorkflowManager from "../authentication-workflow/authenticationWorkflowManager"; + import config from "../agora-manager/config"; + import { useAgoraContext } from "../agora-manager/agoraManager"; + ``` + diff --git a/assets/code/video-sdk/product-workflow/ios-extension.mdx b/assets/code/video-sdk/product-workflow/ios-extension.mdx new file mode 100644 index 000000000..4bdc8963d --- /dev/null +++ b/assets/code/video-sdk/product-workflow/ios-extension.mdx @@ -0,0 +1,80 @@ + iOS uses a separate target for screen sharing that uses `ReplayKit`. With the `ReplayKit` package product you can get the screen stream back into the main app with ease. + + 1. Create a new Broadcast Upload Extension target, add Agora's ReplayKit package product to the new target and delete the `SampleHandler` swift file that is automatically created. + + 1. Modify the `screenSharer/Info.plist` file to contain the following key/value pair: + + ```xml + NSExtensionPrincipalClass + AgoraReplayKitHandler + ``` + + 1. Add an [`RPSystemBroadcastPickerView`](https://developer.apple.com/documentation/replaykit/rpsystembroadcastpickerview) to your main app target. + + 1. Set up screen sharing app socket: + + ```swift + func setupScreenSharing() { + let capParams = AgoraScreenCaptureParameters2() + capParams.captureAudio = false + capParams.captureVideo = true + agoraEngine.startScreenCapture(capParams) + } + ``` + + - AgoraScreenCaptureParameters2 + - startScreenCapture(_:) + + 1. Listen for the app extension start and stop: + + ```swift + public func rtcEngine( + _ engine: AgoraRtcEngineKit, localVideoStateChangedOf state: AgoraVideoLocalState, + error: AgoraLocalVideoStreamError, sourceType: AgoraVideoSourceType + ) { + // This delegate method catches whenever a screen is being shared + // from a broadcast extension + if sourceType == .screen { + let connection = AgoraRtcConnection( + channelId: <#Channel Name#>, + localUid: screenShareID + ) + switch state { + case .capturing: + self.publishScreenCaptureTrack(connection) + case .encoding: break + case .stopped, .failed: + // The broadcast extension has finished capturing frames + agoraEngine.leaveChannelEx(connection) + @unknown default: break + } + } + } + ``` + + - rtcEngine(_:localVideoStateChangedOf:error:sourceType:) + - AgoraRtcConnection + - leaveChannelEx(_:leaveChannelBlock:) + + 1. Publish the screen capture: + + ```swift + fileprivate func publishScreenCaptureTrack(_ connection: AgoraRtcConnection) { + /* The broadcast extension has started capturing frames */ + let mediaOptions = AgoraRtcChannelMediaOptions() + mediaOptions.publishCameraTrack = false + mediaOptions.publishMicrophoneTrack = false + mediaOptions.publishScreenCaptureAudio = false + mediaOptions.publishScreenCaptureVideo = true + mediaOptions.clientRoleType = .broadcaster + mediaOptions.autoSubscribeAudio = false + + agoraEngine.joinChannelEx( + byToken: <#Screen share token#>, connection: connection, + delegate: nil, mediaOptions: mediaOptions + ) + } + ``` + + - AgoraRtcChannelMediaOptions + - joinChannelEx(byToken:connection:delegate:mediaOptions:joinSuccess:) diff --git a/assets/code/video-sdk/product-workflow/macos-screencapture.mdx b/assets/code/video-sdk/product-workflow/macos-screencapture.mdx new file mode 100644 index 000000000..028937706 --- /dev/null +++ b/assets/code/video-sdk/product-workflow/macos-screencapture.mdx @@ -0,0 +1,88 @@ + +1. **Get available screens and windows** + + ```swift + func getScreensAndWindows() -> [AgoraScreenCaptureSourceInfo]? { + agoraEngine.getScreenCaptureSources( + withThumbSize: .zero, + iconSize: .zero, + includeScreen: true + ) + } + ``` + + - getScreenCaptureSources(withThumbSize:iconSize:includeScreen:) + - AgoraScreenCaptureSourceInfo + + `getScreenCaptureSources` fetches all the available windows currently running on your mac, and if `includeScreen` is set to true, it also fetches the available screens. + + The `AgoraScreenCaptureSourceInfo` object contains useful information such as the application it comes from (found in sourceName), as well as a snapshot of that screen/window, and a thumbnail of the application source. + +1. **Start capturing the screen or window** + + The `AgoraScreenCaptureSourceInfo/sourceId` property shows its matching `CGWindowID` for either the window or screen. + + ```swift + func startScreenShare(displayId: CGWindowID) { + let params = AgoraScreenCaptureParameters() + params.dimensions = AgoraVideoDimension1920x1080 + params.frameRate = AgoraVideoFrameRate.fps15.rawValue + self.agoraEngine.startScreenCapture( + byDisplayId: displayId, regionRect: .zero, + captureParams: params + ) + } + + func startScreenShare(windowId: CGWindowID) { + let params = AgoraScreenCaptureParameters() + params.dimensions = AgoraVideoDimension1920x1080 + params.frameRate = AgoraVideoFrameRate.fps15.rawValue + self.agoraEngine.startScreenCapture( + byWindowId: windowId, regionRect: .zero, + captureParams: params + ) + } + ``` + + - AgoraScreenCaptureParameters + - startScreenCapture(byWindowId:regionRect:captureParams:) + - startScreenCapture(byDisplayId:regionRect:captureParams:) + +1. **Listen for screenshare update events** + + Switch between publishing your camera feed or screen share feed. + + ```swift + public func rtcEngine( + _ engine: AgoraRtcEngineKit, localVideoStateChangedOf state: AgoraVideoLocalState, + error: AgoraLocalVideoStreamError, sourceType: AgoraVideoSourceType + ) { + if sourceType == .screen { + let newChannelOpt = AgoraRtcChannelMediaOptions() + switch state { + case .capturing: + newChannelOpt.publishScreenTrack = true + newChannelOpt.publishCameraTrack = false + case .stopped, .failed: + newChannelOpt.publishScreenTrack = false + newChannelOpt.publishCameraTrack = true + default: return + } + agoraEngine.updateChannel(with: newChannelOpt) + } + } + ``` + + - rtcEngine(_:localVideoStateChangedOf:error:sourceType:) + - AgoraRtcChannelMediaOptions + - updateChannel(with:) + +1. **Stop sharing** + + ```swift + func stopScreenShare() { + self.agoraEngine.stopScreenCapture() + } + ``` + + - stopScreenCapture() \ No newline at end of file diff --git a/assets/code/video-sdk/product-workflow/media-device-changed.mdx b/assets/code/video-sdk/product-workflow/media-device-changed.mdx new file mode 100644 index 000000000..86bc8889c --- /dev/null +++ b/assets/code/video-sdk/product-workflow/media-device-changed.mdx @@ -0,0 +1,86 @@ + + + + ```typescript + const OnMicrophoneChangedHook: React.FC = () => { + const agoraContext = useAgoraContext(); + + useEffect(() => { + const onMicrophoneChanged = (changedDevice: DeviceInfo) => { + if (changedDevice.state === "ACTIVE") { + agoraContext.localMicrophoneTrack?.setDevice(changedDevice.device.deviceId) + .catch((error: IAgoraRTCError) => console.error(error)); + } else if (changedDevice.device.label === agoraContext.localMicrophoneTrack?.getTrackLabel()) { + AgoraRTC.getMicrophones() + .then((devices) => agoraContext.localMicrophoneTrack?.setDevice(devices[0].deviceId)) + .catch((error) => console.error(error)); + } + }; + AgoraRTC.onMicrophoneChanged = onMicrophoneChanged; + + return () => { + AgoraRTC.onMicrophoneChanged = undefined; + }; + }, [agoraContext.localMicrophoneTrack]); + return null; + }; + + const OnCameraChangedHook: React.FC = () => { + const agoraContext = useAgoraContext(); + + useEffect(() => { + const onCameraChanged = (changedDevice: DeviceInfo) => { + if (changedDevice.state === "ACTIVE") { + agoraContext.localCameraTrack?.setDevice(changedDevice.device.deviceId) + .catch((error) => console.error(error)); + } else if (changedDevice.device.label === agoraContext.localCameraTrack?.getTrackLabel()) { + AgoraRTC.getCameras() + .then((devices) => agoraContext.localCameraTrack?.setDevice(devices[0].deviceId)) + .catch((error) => console.error(error)); + } + }; + + AgoraRTC.onCameraChanged = onCameraChanged; + return () => { + AgoraRTC.onCameraChanged = undefined; + }; + }, [agoraContext.localCameraTrack]); + return null; + }; + ``` + - onMicrophoneChanged + - onCameraChanged + + + + + ```typescript + const OnMicrophoneChangedHook: React.FC = () => { + const agoraContext = useAgoraContext(); + + useEffect(() => { + const onMicrophoneChanged = (changedDevice: DeviceInfo) => { + if (changedDevice.state === "ACTIVE") { + agoraContext.localMicrophoneTrack?.setDevice(changedDevice.device.deviceId) + .catch((error: IAgoraRTCError) => console.error(error)); + } else if (changedDevice.device.label === agoraContext.localMicrophoneTrack?.getTrackLabel()) { + AgoraRTC.getMicrophones() + .then((devices) => agoraContext.localMicrophoneTrack?.setDevice(devices[0].deviceId)) + .catch((error) => console.error(error)); + } + }; + AgoraRTC.onMicrophoneChanged = onMicrophoneChanged; + + return () => { + AgoraRTC.onMicrophoneChanged = undefined; + }; + }, [agoraContext.localMicrophoneTrack]); + return null; + }; + + ``` + - onMicrophoneChanged + + + + \ No newline at end of file diff --git a/assets/code/video-sdk/product-workflow/microphone-camera-change.mdx b/assets/code/video-sdk/product-workflow/microphone-camera-change.mdx new file mode 100644 index 000000000..66095041f --- /dev/null +++ b/assets/code/video-sdk/product-workflow/microphone-camera-change.mdx @@ -0,0 +1,79 @@ + + +To prevent web pages from playing sounds without the user's consent, browsers impose restrictions on the Autoplay function. This means that audio cannot play automatically without user interaction, such as a click or touch. These limitations are in place to enhance the user experience, as sudden and automatic audio playback upon visiting a web page may not align with the user's preferences. Most browsers do not apply the autoplay policy to pure video content. However, in the Safari browser, when low-power mode is activated, and custom autoplay restrictions are enabled (as seen in the iOS WKWebView, as well as some iOS WeChat browser configurations), pure video Autoplay may also be restricted. + +When using Agora SDK for Web, you can deal with this in the following ways: + +- Listen for `onAutoplayFailedcallbacks`, and use the callbacks to display a button on the page to guide the user to click, thus lifting the browser's audio or video autoplay restrictions. +- In product design, ensure that the user has interacted with the page before calling `play`. + +```javascript + AgoraRTC.onAutoplayFailed = () => { + // Create button for the user interaction. + const btn = document.createElement("button"); + // Set the button text. + btn.innerText = "Click me to resume the audio/video playback"; + // Remove the button when onClick event occurs. + btn.onClick = () => { + btn.remove(); + }; + // Append the button to the UI. + document.body.append(btn); + }; + AgoraRTC.onMicrophoneChanged = async (changedDevice) => { + eventsCallback("microphone-changed", changedDevice) + }; + + AgoraRTC.onCameraChanged = async (changedDevice) => { + eventsCallback("camera-changed", changedDevice) + }; +``` +- onAudioAutoplayFailed + +In `eventsCallback` you can handle the events as follows: + +```javascript + const handleVSDKEvents = async (eventName, ...args) => { + switch (eventName) { + // ... other cases + case "microphone-changed": + // When plugging in a device, switch to a device that is newly plugged in. + if (changedDevice.state === "ACTIVE") { + channelParameters.localAudioTrack.setDevice( + changedDevice.device.deviceId + ); + // Switch to an existing device when the current device is unplugged. + } else if ( + changedDevice.device.label === + channelParameters.localAudioTrack.getTrackLabel() + ) { + const oldMicrophones = await AgoraRTC.getMicrophones(); + oldMicrophones[0] && + channelParameters.localAudioTrack.setDevice( + oldMicrophones[0].deviceId + ); + } + case "camera-changed": + // When plugging in a device, switch to a device that is newly plugged in. + if (changedDevice.state === "ACTIVE") { + channelParameters.localVideoTrack.setDevice( + changedDevice.device.deviceId + ); + // Switch to an existing device when the current device is unplugged. + } else if ( + changedDevice.device.label === + channelParameters.localVideoTrack.getTrackLabel() + ) { + const oldCameras = await AgoraRTC.getCameras(); + oldCameras[0] && + channelParameters.localVideoTrack.setDevice(oldCameras[0].deviceId); + } + } + }; +``` +- onMicrophoneChanged +- onCameraChanged +- IMicrophoneAudioTrack.setDevice +- ICameraVideoTrack.setDevice + + diff --git a/assets/code/video-sdk/product-workflow/mute-local-video.mdx b/assets/code/video-sdk/product-workflow/mute-local-video.mdx new file mode 100644 index 000000000..7345b4456 --- /dev/null +++ b/assets/code/video-sdk/product-workflow/mute-local-video.mdx @@ -0,0 +1,21 @@ + +```typescript +const MuteVideoComponent: React.FC = () => { + const agoraContext = useAgoraContext(); + const [isMuteVideo, setMuteVideo] = useState(false); + + const toggleMuteVideo = () => { + agoraContext.localCameraTrack + ?.setEnabled(isMuteVideo) + .then(() => setMuteVideo((prev) => !prev)) + .catch((error) => console.error(error)); + }; + + return ( + + ); +}; +``` + - setEnabled + + \ No newline at end of file diff --git a/assets/code/video-sdk/product-workflow/mute-remote-user.mdx b/assets/code/video-sdk/product-workflow/mute-remote-user.mdx new file mode 100644 index 000000000..732cef884 --- /dev/null +++ b/assets/code/video-sdk/product-workflow/mute-remote-user.mdx @@ -0,0 +1,77 @@ + + ```kotlin + fun mute(muted: Boolean) { + // Stop or resume publishing the local video stream + agoraEngine?.muteLocalAudioStream(muted) + // Stop or resume subscribing to the video streams of all remote users + agoraEngine?.muteAllRemoteAudioStreams(muted) + // Stop or resume subscribing to the audio stream of a specified user + // agoraEngine?.muteRemoteAudioStream(remoteUid, muted) + } + ``` + - muteLocalAudioStream + + - muteRemoteAudioStream + + - muteAllRemoteAudioStreams + + - enableLocalAudio + + + + ```csharp + public void MuteRemoteAudio(bool value) + { + if (remoteUid > 0) + { + // Pass the uid of the remote user you want to mute. + agoraEngine.MuteRemoteAudioStream(remoteUid, value); + } + else + { + Debug.Log("No remote user in the channel"); + } + } + ``` + + - MuteRemoteAudioStream + + + - MuteRemoteAudioStream + + + + ```swift + func muteRemoteUser(uid: UInt, isMuted: Bool) { + self.agoraEngine.muteRemoteAudioStream(uid, mute: isMuted) + } + ``` + + + - muteRemoteAudioStream(_:​mute:) + + + - muteRemoteAudioStream(_:​mute:) + + + +```javascript + // Mute and unmute the local video. + document.getElementById("muteVideo").onclick = async function () { + if (isMuteVideo == false) { + // Mute the local video. + channelParameters.localVideoTrack.setEnabled(false); + // Update the button text. + document.getElementById(`muteVideo`).innerHTML = "Unmute Video"; + isMuteVideo = true; + } else { + // Unmute the local video. + channelParameters.localVideoTrack.setEnabled(true); + // Update the button text. + document.getElementById(`muteVideo`).innerHTML = "Mute Video"; + isMuteVideo = false; + } + }; +``` +- setEnabled + diff --git a/assets/code/video-sdk/product-workflow/override-broadcast-started.mdx b/assets/code/video-sdk/product-workflow/override-broadcast-started.mdx new file mode 100644 index 000000000..40c09d14b --- /dev/null +++ b/assets/code/video-sdk/product-workflow/override-broadcast-started.mdx @@ -0,0 +1,25 @@ + + ```swift + override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) { + guard let channel = UserDefaults(suiteName: "group.uk.rocketar.Docs-Examples")?.string(forKey: "channel") else { + // Failed to get channel + self.broadcastFinished() + return + } + let channelMediaOptions = AgoraRtcChannelMediaOptions() + channelMediaOptions.publishMicrophoneTrack = false + channelMediaOptions.publishCameraTrack = false + channelMediaOptions.publishCustomVideoTrack = true + channelMediaOptions.publishCustomAudioTrack = true + channelMediaOptions.autoSubscribeAudio = false + channelMediaOptions.autoSubscribeVideo = false + channelMediaOptions.clientRoleType = .broadcaster + + engine.joinChannel( + byToken: DocsAppConfig.shared.rtcToken, channelId: channel, + uid: DocsAppConfig.shared.screenShareId, + mediaOptions: channelMediaOptions + ) + } + ``` + \ No newline at end of file diff --git a/assets/code/video-sdk/product-workflow/preview-screen-track.mdx b/assets/code/video-sdk/product-workflow/preview-screen-track.mdx new file mode 100644 index 000000000..f76f25d2c --- /dev/null +++ b/assets/code/video-sdk/product-workflow/preview-screen-track.mdx @@ -0,0 +1,40 @@ + + ```kotlin + fun screenShareSurfaceView(): SurfaceView { + // Create render view by RtcEngine + val surfaceView = SurfaceView(mContext) + // Setup and return a SurfaceView to render your screen sharing preview + agoraEngine?.startPreview(Constants.VideoSourceType.VIDEO_SOURCE_SCREEN_PRIMARY) + agoraEngine?.setupLocalVideo(VideoCanvas(surfaceView, Constants.RENDER_MODE_FIT, 0)) + return surfaceView + } + ``` + - startPreview + - setupLocalVideo + + + + ```csharp + public void PlayScreenTrackLocally(bool isScreenSharing, GameObject localViewGo) + { + if (isScreenSharing) + { + // Update the VideoSurface component of the local view GameObject. + LocalView = localViewGo.AddComponent(); + // Render the screen sharing track on the local view GameObject. + LocalView.SetForUser(0, "", VIDEO_SOURCE_TYPE.VIDEO_SOURCE_SCREEN_PRIMARY); + } + else + { + // Update the VideoSurface component of the local view GameObject. + LocalView = localViewGo.AddComponent(); + // Render the local video track on the local view GameObject. + LocalView.SetForUser(0, "", VIDEO_SOURCE_TYPE.VIDEO_SOURCE_CAMERA_PRIMARY); + } + } + ``` + + - SetForUser + - VIDEO_SOURCE_TYPE + + \ No newline at end of file diff --git a/assets/code/video-sdk/product-workflow/publish-screen-track.mdx b/assets/code/video-sdk/product-workflow/publish-screen-track.mdx new file mode 100644 index 000000000..a6d2c6f1e --- /dev/null +++ b/assets/code/video-sdk/product-workflow/publish-screen-track.mdx @@ -0,0 +1,40 @@ + + ```kotlin + private fun updateMediaPublishOptions(publishScreen: Boolean) { + val mediaOptions = ChannelMediaOptions() + mediaOptions.publishCameraTrack = !publishScreen + mediaOptions.publishMicrophoneTrack = !publishScreen + mediaOptions.publishScreenCaptureVideo = publishScreen + mediaOptions.publishScreenCaptureAudio = publishScreen + agoraEngine!!.updateChannelMediaOptions(mediaOptions) + } + ``` + - updateChannelMediaOptions + + + ```csharp + public void PublishScreenTrack() + { + // Publish the screen track + ChannelMediaOptions channelOptions = new ChannelMediaOptions(); + channelOptions.publishScreenTrack.SetValue(true); + channelOptions.publishMicrophoneTrack.SetValue(true); + channelOptions.publishSecondaryScreenTrack.SetValue(true); + channelOptions.publishCameraTrack.SetValue(false); + agoraEngine.UpdateChannelMediaOptions(channelOptions); + } + + public void UnPublishScreenTrack() + { + // Unpublish the screen track. + ChannelMediaOptions channelOptions = new ChannelMediaOptions(); + channelOptions.publishScreenTrack.SetValue(false); + channelOptions.publishCameraTrack.SetValue(true); + channelOptions.publishMicrophoneTrack.SetValue(true); + agoraEngine.UpdateChannelMediaOptions(channelOptions); + } + ``` + - UpdateChannelMediaOptions + - ChannelMediaOptions + + \ No newline at end of file diff --git a/assets/code/video-sdk/product-workflow/screen-sharer-target.mdx b/assets/code/video-sdk/product-workflow/screen-sharer-target.mdx new file mode 100644 index 000000000..367c0c910 --- /dev/null +++ b/assets/code/video-sdk/product-workflow/screen-sharer-target.mdx @@ -0,0 +1,23 @@ + + ```swift + class SampleHandler: RPBroadcastSampleHandler, AgoraRtcEngineDelegate { + var engine: AgoraRtcEngineKit { + let config = AgoraRtcEngineConfig() + config.appId = DocsAppConfig.shared.appId + config.channelProfile = .liveBroadcasting + let agoraEngine = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + agoraEngine.enableVideo() + agoraEngine.setExternalVideoSource(true, useTexture: true, sourceType: .videoFrame) + let videoConfig = AgoraVideoEncoderConfiguration( + size: videoDimension, frameRate: .fps10, bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative, mirrorMode: .auto + ) + agoraEngine.setVideoEncoderConfiguration(videoConfig) + + agoraEngine.setAudioProfile(.default) + agoraEngine.setExternalAudioSource(true, sampleRate: 44100, channels: 2) + return agoraEngine + } + } + ``` + diff --git a/assets/code/video-sdk/product-workflow/setup-engine.mdx b/assets/code/video-sdk/product-workflow/setup-engine.mdx new file mode 100644 index 000000000..2640684e2 --- /dev/null +++ b/assets/code/video-sdk/product-workflow/setup-engine.mdx @@ -0,0 +1,20 @@ + +```typescript +export function ProductWorkflow() { + const agoraEngine = useRTCClient(AgoraRTC.createClient({ codec: "vp8", mode: config.selectedProduct })); + + return ( +
+

Screen share, volume control and mute

+ + + + + +
+ ); +} +``` + - useRTCClient + - AgoraRTCProvider +
\ No newline at end of file diff --git a/assets/code/video-sdk/product-workflow/setup-volume.mdx b/assets/code/video-sdk/product-workflow/setup-volume.mdx new file mode 100644 index 000000000..64a0b139f --- /dev/null +++ b/assets/code/video-sdk/product-workflow/setup-volume.mdx @@ -0,0 +1,153 @@ + + + ```kotlin + fun adjustVolume(volumeParameter: VolumeTypes, volume: Int) { + when (volumeParameter) { + VolumeTypes.PLAYBACK_SIGNAL_VOLUME -> { + agoraEngine?.adjustPlaybackSignalVolume(volume) + } + VolumeTypes.RECORDING_SIGNAL_VOLUME -> { + agoraEngine?.adjustRecordingSignalVolume(volume) + } + VolumeTypes.USER_PLAYBACK_SIGNAL_VOLUME -> { + if (remoteUids.size > 0) { + val remoteUid = remoteUids.first() // the uid of the remote user + agoraEngine?.adjustUserPlaybackSignalVolume(remoteUid, volume) + } + } + VolumeTypes.AUDIO_MIXING_VOLUME -> { + agoraEngine?.adjustAudioMixingVolume(volume) + } + VolumeTypes.AUDIO_MIXING_PLAYOUT_VOLUME -> { + agoraEngine?.adjustAudioMixingPlayoutVolume(volume) + } + VolumeTypes.AUDIO_MIXING_PUBLISH_VOLUME -> { + agoraEngine?.adjustAudioMixingPublishVolume(volume) + } + VolumeTypes.CUSTOM_AUDIO_PLAYOUT_VOLUME -> { + agoraEngine?.adjustAudioMixingPlayoutVolume(volume) + } + VolumeTypes.CUSTOM_AUDIO_PUBLISH_VOLUME -> { + val trackId = 0 // use the id of your custom audio track + agoraEngine?.adjustCustomAudioPublishVolume(trackId, volume) + } + } + } + ``` + - adjustPlaybackSignalVolume + + - adjustRecordingSignalVolume + + - adjustUserPlaybackSignalVolume + + - adjustAudioMixingVolume + + - adjustAudioMixingPlayoutVolume + + - adjustAudioMixingPublishVolume + + - setInEarMonitoringVolume + + - adjustCustomAudioPlayoutVolume + + - adjustCustomAudioPublishVolume + + + ```swift + func setVolume(for id: UInt, to volume: Int) -> Int32 { + if id == self.localUserId { + return self.agoraEngine.adjustRecordingSignalVolume(volume) + } else { + return self.agoraEngine.adjustUserPlaybackSignalVolume(id, volume: Int32(volume)) + } + } + ``` + + + - adjustRecordingSignalVolume(_:) + - adjustUserPlaybackSignalVolume(_:volume:) + + + - adjustRecordingSignalVolume(_:) + - adjustUserPlaybackSignalVolume(_:volume:) + + + + ```csharp + public void ChangeVolume(int volume) + { + // Adjust the volume of the recorded signal. + agoraEngine.AdjustRecordingSignalVolume(volume); + } + ``` + + - AdjustRecordingSignalVolume + + + - AdjustRecordingSignalVolume + + + +```javascript + // Set an event listener on the range slider. + document + .getElementById("localAudioVolume") + .addEventListener("change", function (evt) { + console.log("Volume of local audio :" + evt.target.value); + // Set the local audio volume. + channelParameters.localAudioTrack.setVolume(parseInt(evt.target.value)); + }); + + // Set an event listener on the range slider. + document + .getElementById("remoteAudioVolume") + .addEventListener("change", function (evt) { + console.log("Volume of remote audio :" + evt.target.value); + // Set the remote audio volume. + channelParameters.remoteAudioTrack.setVolume(parseInt(evt.target.value)); + }); +``` +- setVolume + + + ```typescript + const RemoteAndLocalVolumeComponent: React.FC = () => { + const agoraContext = useAgoraContext(); + const remoteUsers = useRemoteUsers(); + const numberOfRemoteUsers = remoteUsers.length; + const remoteUser = remoteUsers[numberOfRemoteUsers - 1]; + + const handleLocalAudioVolumeChange = (evt: React.ChangeEvent) => { + const volume = parseInt(evt.target.value); + console.log("Volume of local audio:", volume); + agoraContext.localMicrophoneTrack?.setVolume(volume); + }; + + const handleRemoteAudioVolumeChange = (evt: React.ChangeEvent) => { + if (remoteUser) { + const volume = parseInt(evt.target.value); + console.log("Volume of remote audio:", volume); + remoteUser.audioTrack?.setVolume(volume); + } else { + console.log("No remote user in the channel"); + } + }; + + return ( + <> +
+ + +
+
+ + +
+ + ); + } + ``` + - useRemoteUsers + - localMicrophoneTrack.setVolume + - remoteUser.audioTrack.setVolume +
diff --git a/assets/code/video-sdk/product-workflow/start-sharing.mdx b/assets/code/video-sdk/product-workflow/start-sharing.mdx new file mode 100644 index 000000000..23979c36c --- /dev/null +++ b/assets/code/video-sdk/product-workflow/start-sharing.mdx @@ -0,0 +1,167 @@ + + ```kotlin + fun startScreenSharing() { + // Set screen capture parameters + val screenCaptureParameters = ScreenCaptureParameters() + screenCaptureParameters.captureVideo = true + screenCaptureParameters.captureAudio = true + screenCaptureParameters.videoCaptureParameters.framerate = 15 + screenCaptureParameters.audioCaptureParameters.captureSignalVolume = 100 + + // Start screen sharing + agoraEngine!!.startScreenCapture(screenCaptureParameters) + // Update channel media options to publish the screen sharing video stream + updateMediaPublishOptions(true) + } + ``` + - ScreenCaptureParameters + - startScreenCapture + + + ```swift + class SampleHandler: RPBroadcastSampleHandler, AgoraRtcEngineDelegate { + var engine: AgoraRtcEngineKit { + let config = AgoraRtcEngineConfig() + config.appId = DocsAppConfig.shared.appId + config.channelProfile = .liveBroadcasting + let agoraEngine = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + agoraEngine.enableVideo() + agoraEngine.setExternalVideoSource(true, useTexture: true, sourceType: .videoFrame) + let videoConfig = AgoraVideoEncoderConfiguration( + size: videoDimension, frameRate: .fps10, bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative, mirrorMode: .auto + ) + agoraEngine.setVideoEncoderConfiguration(videoConfig) + + agoraEngine.setAudioProfile(.default) + agoraEngine.setExternalAudioSource(true, sampleRate: 44100, channels: 2) + return agoraEngine + } + } + ``` + + + 1. Get a list of shareable screens + + ```csharp + // Get the list of shareable screens + private ScreenCaptureSourceInfo[] GetScreenCaptureSources() + { + SIZE targetSize = new SIZE(360, 660); + return agoraEngine.GetScreenCaptureSources(targetSize, targetSize, true); + } + ``` + For more details, see the following: + - GetScreenCaptureSources + - ScreenCaptureSourceInfo + + 1. Start screen capture and share the screen + + ```csharp + private void StartScreenCaptureMobile(long sourceId) + { + // Configure screen capture parameters for Android. + var parameters2 = new ScreenCaptureParameters2(); + parameters2.captureAudio = true; + parameters2.captureVideo = true; + // Start screen sharing. + agoraEngine.StartScreenCapture(parameters2); + } + + private void StartScreenCaptureWindows(long sourceId) + { + // Configure screen capture parameters for Windows. + agoraEngine.StartScreenCaptureByDisplayId((uint)sourceId, default(Rectangle), + new ScreenCaptureParameters { captureMouseCursor = true, frameRate = 30 }); + } + // Share the screen + public void StartSharing() + { + if (agoraEngine == null) + { + Debug.Log("Join a channel to start screen sharing"); + return; + } + + // Get a list of shareable screens and windows. + var captureSources = GetScreenCaptureSources(); + + if (captureSources != null && captureSources.Length > 0) + { + var sourceId = captureSources[0].sourceId; + + // Start screen sharing based on platform. + #if UNITY_ANDROID || UNITY_IPHONE + StartScreenCaptureMobile(sourceId); + #else + StartScreenCaptureWindows(sourceId); + #endif + + // Publish the screen track. + PublishScreenTrack(); + } + else + { + Debug.LogWarning("No screen capture sources found."); + } + } + ``` + - StartScreenCaptureByDisplayId + - StartScreenCapture + + +```javascript + const startScreenShare = async (channelParameters, screenPlayerContainer) => { + // Create a screen track for screen sharing. + channelParameters.screenTrack = await AgoraRTC.createScreenVideoTrack(); + await agoraManager + .getAgoraEngine() + .unpublish([channelParameters.localVideoTrack]); + channelParameters.localVideoTrack.close(); + // Replace the video track with the screen track. + await agoraManager + .getAgoraEngine() + .publish([channelParameters.screenTrack]); + // Play the screen track. + channelParameters.screenTrack.play(screenPlayerContainer); + }; +``` +- createScreenVideoTrack + + + ```typescript + const ShareScreenComponent: React.FC<{ setScreenSharing: React.Dispatch> }> = ({ + setScreenSharing, + }) => { + const screenShareClient = useRef(AgoraRTC.createClient({ codec: "vp8", mode: "rtc" })); + const { screenTrack, isLoading, error } = useLocalScreenTrack(true, {}, "disable", screenShareClient.current); + + useJoin({ + appid: config.appId, + channel: config.channelName, + token: config.rtcToken, + uid: 0, + }, true, screenShareClient.current); + + useTrackEvent(screenTrack, "track-ended", () => { + setScreenSharing(false); + }); + useEffect(() => { + if (error) setScreenSharing(false); + }, [error, setScreenSharing]); + + usePublish([screenTrack], screenTrack !== null, screenShareClient.current); + + if (isLoading) { + return

Sharing screen...

; + } + return <>; + }; + ``` + - useRTCClient + - useLocalScreenTrack + - useJoin + - useTrackEvent + - usePublish + +
diff --git a/assets/code/video-sdk/product-workflow/stop-sharing.mdx b/assets/code/video-sdk/product-workflow/stop-sharing.mdx new file mode 100644 index 000000000..a25c929ed --- /dev/null +++ b/assets/code/video-sdk/product-workflow/stop-sharing.mdx @@ -0,0 +1,41 @@ + + ```kotlin + fun stopScreenSharing() { + agoraEngine!!.stopScreenCapture() + // Restore camera and microphone publishing + updateMediaPublishOptions(false) + } + ``` + - stopScreenCapture + + + ```csharp + public void StopSharing() + { + // Stop screen sharing. + agoraEngine.StopScreenCapture(); + + // Publish the local video track when you stop sharing your screen. + UnPublishScreenTrack(); + + } + ``` + - StopScreenCapture + + +```javascript + const stopScreenShare = async (channelParameters, localPlayerContainer) => { + // Replace the screen track with the video track. + await agoraManager + .getAgoraEngine() + .unpublish([channelParameters.screenTrack]); + channelParameters.screenTrack.close(); + channelParameters.localVideoTrack = await AgoraRTC.createCameraVideoTrack(); + await agoraManager + .getAgoraEngine() + .publish([channelParameters.localVideoTrack]); + // Play the video track. + channelParameters.localVideoTrack.play(localPlayerContainer); + }; +``` + diff --git a/assets/code/video-sdk/raw-video-audio/configure-engine.mdx b/assets/code/video-sdk/raw-video-audio/configure-engine.mdx new file mode 100644 index 000000000..3fe89ddbf --- /dev/null +++ b/assets/code/video-sdk/raw-video-audio/configure-engine.mdx @@ -0,0 +1,77 @@ + + + ```csharp + public override void SetupAgoraEngine() + { + var bufferLength = SAMPLE_RATE * CHANNEL; // 1-sec-length buffer + _audioBuffer = new RingBuffer(bufferLength, true); + var canvas = GameObject.Find("Canvas"); + var aud = canvas.AddComponent(); + SetupAudio(aud, "externalClip"); + base.SetupAgoraEngine(); + InitializeTexture(); + agoraEngine.InitEventHandler(new UserEventHandler(this)); + agoraEngine.RegisterVideoFrameObserver(new RawAudioVideoEventHandler(this), + VIDEO_OBSERVER_FRAME_TYPE.FRAME_TYPE_RGBA, + VIDEO_OBSERVER_POSITION.POSITION_POST_CAPTURER | + VIDEO_OBSERVER_POSITION.POSITION_PRE_RENDERER | + VIDEO_OBSERVER_POSITION.POSITION_PRE_ENCODER, + OBSERVER_MODE.RAW_DATA); + SetVideoEncoderConfiguration(); + agoraEngine.SetPlaybackAudioFrameParameters(SAMPLE_RATE, CHANNEL, + RAW_AUDIO_FRAME_OP_MODE_TYPE.RAW_AUDIO_FRAME_OP_MODE_READ_ONLY, 1024); + agoraEngine.SetRecordingAudioFrameParameters(SAMPLE_RATE, CHANNEL, + RAW_AUDIO_FRAME_OP_MODE_TYPE.RAW_AUDIO_FRAME_OP_MODE_READ_ONLY, 1024); + agoraEngine.SetMixedAudioFrameParameters(SAMPLE_RATE, CHANNEL, 1024); + agoraEngine.SetEarMonitoringAudioFrameParameters(SAMPLE_RATE, CHANNEL, + RAW_AUDIO_FRAME_OP_MODE_TYPE.RAW_AUDIO_FRAME_OP_MODE_READ_ONLY, 1024); + agoraEngine.RegisterAudioFrameObserver(new RawAudioEventHandler(this), + AUDIO_FRAME_POSITION.AUDIO_FRAME_POSITION_PLAYBACK | + AUDIO_FRAME_POSITION.AUDIO_FRAME_POSITION_RECORD | + AUDIO_FRAME_POSITION.AUDIO_FRAME_POSITION_MIXED | + AUDIO_FRAME_POSITION.AUDIO_FRAME_POSITION_BEFORE_MIXING | + AUDIO_FRAME_POSITION.AUDIO_FRAME_POSITION_EAR_MONITORING, + OBSERVER_MODE.RAW_DATA); + } + ``` + - InitEventHandler + - RegisterVideoFrameObserver + - SetPlaybackAudioFrameParameters + - SetRecordingAudioFrameParameters + - SetEarMonitoringAudioFrameParameters + - RegisterAudioFrameObserver + + + ```csharp + public override void SetupAgoraEngine() + { + var bufferLength = SAMPLE_RATE * CHANNEL; // 1-sec-length buffer + _audioBuffer = new RingBuffer(bufferLength, true); + var canvas = GameObject.Find("Canvas"); + var aud = canvas.AddComponent(); + SetupAudio(aud, "externalClip"); + base.SetupAgoraEngine(); + agoraEngine.InitEventHandler(new UserEventHandler(this)); + agoraEngine.SetPlaybackAudioFrameParameters(SAMPLE_RATE, CHANNEL, + RAW_AUDIO_FRAME_OP_MODE_TYPE.RAW_AUDIO_FRAME_OP_MODE_READ_ONLY, 1024); + agoraEngine.SetRecordingAudioFrameParameters(SAMPLE_RATE, CHANNEL, + RAW_AUDIO_FRAME_OP_MODE_TYPE.RAW_AUDIO_FRAME_OP_MODE_READ_ONLY, 1024); + agoraEngine.SetMixedAudioFrameParameters(SAMPLE_RATE, CHANNEL, 1024); + agoraEngine.SetEarMonitoringAudioFrameParameters(SAMPLE_RATE, CHANNEL, + RAW_AUDIO_FRAME_OP_MODE_TYPE.RAW_AUDIO_FRAME_OP_MODE_READ_ONLY, 1024); + agoraEngine.RegisterAudioFrameObserver(new RawAudioEventHandler(this), + AUDIO_FRAME_POSITION.AUDIO_FRAME_POSITION_PLAYBACK | + AUDIO_FRAME_POSITION.AUDIO_FRAME_POSITION_RECORD | + AUDIO_FRAME_POSITION.AUDIO_FRAME_POSITION_MIXED | + AUDIO_FRAME_POSITION.AUDIO_FRAME_POSITION_BEFORE_MIXING | + AUDIO_FRAME_POSITION.AUDIO_FRAME_POSITION_EAR_MONITORING, + OBSERVER_MODE.RAW_DATA); + } + ``` + - InitEventHandler + - SetPlaybackAudioFrameParameters + - SetRecordingAudioFrameParameters + - SetEarMonitoringAudioFrameParameters + - RegisterAudioFrameObserver + + \ No newline at end of file diff --git a/assets/code/video-sdk/raw-video-audio/import-library.mdx b/assets/code/video-sdk/raw-video-audio/import-library.mdx new file mode 100644 index 000000000..b2207f0b4 --- /dev/null +++ b/assets/code/video-sdk/raw-video-audio/import-library.mdx @@ -0,0 +1,31 @@ + + + ```kotlin + import io.agora.rtc2.Constants + import io.agora.rtc2.IAudioFrameObserver + import io.agora.rtc2.audio.AudioParams + import io.agora.base.VideoFrame + import io.agora.rtc2.video.IVideoFrameObserver + ``` + + + ```kotlin + import io.agora.rtc2.Constants + import io.agora.rtc2.IAudioFrameObserver + import io.agora.rtc2.audio.AudioParams + ``` + + + + + ```swift + import AgoraRtcKit + ``` + + + ```csharp + using Agora.Rtc; + using RingBuffer; + using UnityEngine.UI; + ``` + \ No newline at end of file diff --git a/assets/code/video-sdk/raw-video-audio/modify-audio-video.mdx b/assets/code/video-sdk/raw-video-audio/modify-audio-video.mdx new file mode 100644 index 000000000..83868e4ca --- /dev/null +++ b/assets/code/video-sdk/raw-video-audio/modify-audio-video.mdx @@ -0,0 +1,177 @@ + + In this example, your modify the captured video frame buffer to crop and scale the frame and play a zoomed-in version of the video. + + ```kotlin + private fun modifyVideoBuffer(videoFrame: VideoFrame) { + if (isZoomed) { + // Read the videoFrame buffer + var buffer = videoFrame.buffer + + val w = buffer.width + val h = buffer.height + val cropX = (w - 320) / 2 + val cropY = (h - 240) / 2 + val cropWidth = 320 + val cropHeight = 240 + val scaleWidth = 320 + val scaleHeight = 240 + + // modify the buffer + buffer = buffer.cropAndScale( + cropX, cropY, + cropWidth, cropHeight, + scaleWidth, scaleHeight + ) + + // replace the videoFrame buffer with the modified buffer + videoFrame.replaceBuffer(buffer, 270, videoFrame.timestampNs) + } + } + ``` + - VideoFrame + - AudioFrame + + + + To modify the video frame: + + ```swift + public class ModifyVideoFrameDelegate: NSObject, AgoraVideoFrameDelegate { + public func onCapture( + _ videoFrame: AgoraOutputVideoFrame, sourceType: AgoraVideoSourceType + ) -> Bool { + // Change the video frame immediately after recording it + true + } + + // Indicate the video frame mode of the observer + public func getVideoFrameProcessMode() -> AgoraVideoFrameProcessMode { + // The process mode of the video frame: readOnly, readWrite + // Default is `.readOnly` function is required to change the output. + .readWrite + } + } + ``` + + + - onCapture(_:sourceType:) + - pixelBuffer + + + - onCapture(_:sourceType:) + - pixelBuffer + + + To modify the audio frame: + + ```swift + public class ModifyAudioFrameDelegate: NSObject, AgoraAudioFrameDelegate { + public func onRecordAudioFrame(_ frame: AgoraAudioFrame, channelId: String) -> Bool { + true + } + } + ``` + + + - onRecordAudioFrame(_:channelId:) + - buffer + + + - onRecordAudioFrame(_:channelId:) + - buffer + + + +1. Convert the raw audio data to a float array: + ```csharp + internal static float[] ConvertByteToFloat16(byte[] byteArray) + { + var floatArray = new float[byteArray.Length / 2]; + for (var i = 0; i < floatArray.Length; i++) + { + floatArray[i] = BitConverter.ToInt16(byteArray, i * 2) / 32768f; // -Int16.MinValue + } + + return floatArray; + } + ``` +1. Set up an audio source to play the recorded audio frame: + ```csharp + void SetupAudio(AudioSource aud, string clipName) + { + _audioClip = AudioClip.Create(clipName, + SAMPLE_RATE / PULL_FREQ_PER_SEC * CHANNEL, + CHANNEL, SAMPLE_RATE, true, + OnAudioRead); + aud.clip = _audioClip; + aud.loop = true; + if (isPlaying) + { + aud.Play(); + } + } + ``` +1. Feed the raw audio frame data to the audio source: + ```csharp + private void OnAudioRead(float[] data) + { + lock (_audioBuffer) + { + for (var i = 0; i < data.Length; i++) + { + if (_audioBuffer.Count > 0) + { + data[i] = _audioBuffer.Get(); + _readCount += 1; + } + } + Debug.Log(string.Format("{0},{1},{2},{3},{4},{5},{6},{7},{8}", data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8])); + } + + //Debug.LogFormat("buffer length remains: {0}", _writeCount - _readCount); + } + ``` + +4. Resize the captured video frame: + ```csharp + public void ResizeVideoFrame() + { + if (!_isTextureAttach) + { + var rd = LocalView.GetComponent(); + rd.texture = _texture; + _isTextureAttach = true; + } + else if (VideoBuffer != null && VideoBuffer.Length != 0 && !_needResize) + { + lock (VideoBuffer) + { + _texture.LoadRawTextureData(VideoBuffer); + _texture.Apply(); + } + } + else if (_needResize) + { + Debug.Log("Resized frame ==> (Width: " + _videoFrameHeight + " Height: " + _videoFrameWidth + ")"); + // Adjust the texture width and height. + _texture.Resize(_videoFrameHeight, _videoFrameHeight); + _texture.Apply(); + _needResize = false; + } + } + ``` + +1. Configure the video encoder according to the resized frame: + ```csharp + // Set video encoder configuration + public void SetVideoEncoderConfiguration() + { + VideoEncoderConfiguration config = new VideoEncoderConfiguration(); + config.dimensions = new VideoDimensions(_videoFrameWidth, _videoFrameHeight); + agoraEngine.SetVideoEncoderConfiguration(config); + } + ``` + - VideoEncoderConfiguration + - SetVideoEncoderConfiguration + + \ No newline at end of file diff --git a/assets/code/video-sdk/raw-video-audio/register-video-audio-frame-observers.mdx b/assets/code/video-sdk/raw-video-audio/register-video-audio-frame-observers.mdx new file mode 100644 index 000000000..492c273c9 --- /dev/null +++ b/assets/code/video-sdk/raw-video-audio/register-video-audio-frame-observers.mdx @@ -0,0 +1,106 @@ + + + To receive callbacks declared in `IVideoFrameObserver` and `IAudioFrameObserver`, you must register the video and audio frame observers with the before joining a channel. To specify the format of audio frames captured by each `IAudioFrameObserver` callback, use the `setRecordingAudioFrameParameters`, `setMixedAudioFrameParameters` and `setPlaybackAudioFrameParameters` methods. + + ```kotlin + override fun joinChannel(channelName: String, token: String?): Int { + // Register the video frame observer + agoraEngine!!.registerVideoFrameObserver(iVideoFrameObserver) + // Register the audio frame observer + agoraEngine!!.registerAudioFrameObserver(iAudioFrameObserver) + + agoraEngine!!.setRecordingAudioFrameParameters( + sampleRate, numberOfChannels, + Constants.RAW_AUDIO_FRAME_OP_MODE_READ_WRITE, samplesPerCall + ) + agoraEngine!!.setPlaybackAudioFrameParameters( + sampleRate, numberOfChannels, + Constants.RAW_AUDIO_FRAME_OP_MODE_READ_WRITE, samplesPerCall + ) + agoraEngine!!.setMixedAudioFrameParameters( + sampleRate, + numberOfChannels, + samplesPerCall + ) + + return super.joinChannel(channelName, token) + } + ``` + - registerAudioFrameObserver + - registerVideoFrameObserver + - setRecordingAudioFrameParameters + - setRecordingAudioFrameParameters + - setPlaybackAudioFrameParameters + + + + To receive callbacks declared in `IAudioFrameObserver`, you must register the audio frame observer with the before joining a channel. To specify the format of audio frames captured by each `IAudioFrameObserver` callback, use the `setRecordingAudioFrameParameters`, `setMixedAudioFrameParameters` and `setPlaybackAudioFrameParameters` methods. + + ```kotlin + override fun joinChannel(channelName: String, token: String?): Int { + // Register the audio frame observer + agoraEngine!!.registerAudioFrameObserver(iAudioFrameObserver) + + agoraEngine!!.setRecordingAudioFrameParameters( + sampleRate, numberOfChannels, + Constants.RAW_AUDIO_FRAME_OP_MODE_READ_WRITE, samplesPerCall + ) + agoraEngine!!.setPlaybackAudioFrameParameters( + sampleRate, numberOfChannels, + Constants.RAW_AUDIO_FRAME_OP_MODE_READ_WRITE, samplesPerCall + ) + agoraEngine!!.setMixedAudioFrameParameters( + sampleRate, + numberOfChannels, + samplesPerCall + ) + + return super.joinChannel(channelName, token) + } + ``` + - registerAudioFrameObserver + - setRecordingAudioFrameParameters + - setRecordingAudioFrameParameters + - setPlaybackAudioFrameParameters + + + + To receive the callbacks that you declared in `AgoraVideoFrameDelegate` and `AgoraAudioFrameDelegate`, you must register the video and audio frame observers with the Agora Engine, before joining a channel. To specify the format of the audio frames captured by each `AgoraAudioFrameDelegate` callback, use the `setRecordingAudioFrameParametersWithSampleRate`, `setPlaybackAudioFrameParametersWithSampleRate` and `setMixedAudioFrameParametersWithSampleRate` methods. + + To do these, add the following lines to the `init()` method of `AgoraManager`: + + ```swift + // Video Setup + self.videoFrameDelegate = ModifyVideoFrameDelegate(modifyController: self) + agoraEngine.setVideoFrameDelegate(videoFrameDelegate) + + // Audio Setup + self.audioFrameDelegate = ModifyAudioFrameDelegate(modifyController: self) + agoraEngine.setAudioFrameDelegate(audioFrameDelegate) + agoraEngine.setRecordingAudioFrameParametersWithSampleRate( + 44100, channel: 1, mode: .readWrite, samplesPerCall: 4410 + ) + agoraEngine.setMixedAudioFrameParametersWithSampleRate( + 44100, channel: 1, samplesPerCall: 4410 + ) + agoraEngine.setPlaybackAudioFrameParametersWithSampleRate( + 44100, channel: 1, mode: .readWrite, samplesPerCall: 4410 + ) + ``` + + + - setVideoFrameDelegate + - setAudioFrameDelegate + - setRecordingAudioFrameParametersWithSampleRate + - setMixedAudioFrameParametersWithSampleRate + - setPlaybackAudioFrameParametersWithSampleRate + + + - setVideoFrameDelegate + - setAudioFrameDelegate + - setRecordingAudioFrameParametersWithSampleRate + - setMixedAudioFrameParametersWithSampleRate + - setPlaybackAudioFrameParametersWithSampleRate + + + \ No newline at end of file diff --git a/assets/code/video-sdk/raw-video-audio/set-audio-frame-observer.mdx b/assets/code/video-sdk/raw-video-audio/set-audio-frame-observer.mdx new file mode 100644 index 000000000..7235b7c04 --- /dev/null +++ b/assets/code/video-sdk/raw-video-audio/set-audio-frame-observer.mdx @@ -0,0 +1,168 @@ + + ```kotlin + private val iAudioFrameObserver: IAudioFrameObserver = object : IAudioFrameObserver { + override fun onRecordAudioFrame( + channelId: String?, + type: Int, + samplesPerChannel: Int, + bytesPerSample: Int, + channels: Int, + samplesPerSec: Int, + buffer: ByteBuffer?, + renderTimeMs: Long, + avsync_type: Int + ): Boolean { + // Gets the captured audio frame. + // Add code here to process the recorded audio. + return false + } + + override fun onPlaybackAudioFrame( + channelId: String?, + type: Int, + samplesPerChannel: Int, + bytesPerSample: Int, + channels: Int, + samplesPerSec: Int, + buffer: ByteBuffer?, + renderTimeMs: Long, + avsync_type: Int + ): Boolean { + // Gets the audio frame for playback. + // Add code here to process the playback audio. + // return true to indicate that Data has been processed + return false + } + + override fun onMixedAudioFrame( + channelId: String?, + type: Int, + samplesPerChannel: Int, + bytesPerSample: Int, + channels: Int, + samplesPerSec: Int, + buffer: ByteBuffer?, + renderTimeMs: Long, + avsync_type: Int + ): Boolean { + // Retrieves the mixed captured and playback audio frame. + return false + } + + override fun onEarMonitoringAudioFrame( + type: Int, + samplesPerChannel: Int, + bytesPerSample: Int, + channels: Int, + samplesPerSec: Int, + buffer: ByteBuffer?, + renderTimeMs: Long, + avsync_type: Int + ): Boolean { + return false + } + + override fun onPlaybackAudioFrameBeforeMixing( + channelId: String?, + userId: Int, + type: Int, + samplesPerChannel: Int, + bytesPerSample: Int, + channels: Int, + samplesPerSec: Int, + buffer: ByteBuffer?, + renderTimeMs: Long, + avsync_type: Int + ): Boolean { + // Retrieves the audio frame of a specified user before mixing. + return false + } + + override fun getObservedAudioFramePosition(): Int { + return 0 + } + + override fun getRecordAudioParams(): AudioParams { + return AudioParams(sampleRate,numberOfChannels, 0 ,samplesPerCall) + } + + override fun getPlaybackAudioParams(): AudioParams { + return AudioParams(sampleRate,numberOfChannels, 0 ,samplesPerCall) + } + + override fun getMixedAudioParams(): AudioParams { + return AudioParams(sampleRate,numberOfChannels, 0 ,samplesPerCall) + } + + override fun getEarMonitoringAudioParams(): AudioParams { + return AudioParams(sampleRate,numberOfChannels, 0 ,samplesPerCall) + } + } + ``` + - IAudioFrameObserver + + + + ```swift + extension ModifyAudioFrameDelegate: AgoraAudioFrameDelegate { + public func onRecordAudioFrame(_ frame: AgoraAudioFrame, channelId: String) -> Bool { + // Change the audio frame immediately after recording it + true + } + public func onPlaybackAudioFrame(_ frame: AgoraAudioFrame, channelId: String) -> Bool { + // Change the audio frame just before playback + true + } + } + ``` + + + - AgoraAudioFrameDelegate + + + - AgoraAudioFrameDelegate + + + + ```csharp + // Internal class for handling audio events + internal class RawAudioEventHandler : IAudioFrameObserver + { + private RawAudioVideoManager _agoraAudioRawData; + + internal RawAudioEventHandler(RawAudioVideoManager agoraAudioRawData) + { + _agoraAudioRawData = agoraAudioRawData; + } + + public override bool OnRecordAudioFrame(string channelId, AudioFrame audioFrame) + { + var floatArray = RawAudioVideoManager.ConvertByteToFloat16(audioFrame.RawBuffer); + + lock (_agoraAudioRawData._audioBuffer) + { + _agoraAudioRawData._audioBuffer.Put(floatArray); + _agoraAudioRawData._writeCount += floatArray.Length; + _agoraAudioRawData._count++; + } + return true; + } + public override bool OnPlaybackAudioFrame(string channelId, AudioFrame audioFrame) + { + return true; + } + public override bool OnPlaybackAudioFrameBeforeMixing(string channel_id, uint uid, AudioFrame audio_frame) + { + return false; + } + + public override bool OnPlaybackAudioFrameBeforeMixing(string channel_id, + string uid, + AudioFrame audio_frame) + { + return false; + } + } + ``` + - IAudioFrameObserver + diff --git a/assets/code/video-sdk/raw-video-audio/set-variables.mdx b/assets/code/video-sdk/raw-video-audio/set-variables.mdx new file mode 100644 index 000000000..b6e917755 --- /dev/null +++ b/assets/code/video-sdk/raw-video-audio/set-variables.mdx @@ -0,0 +1,101 @@ + + + ```kotlin + private var isZoomed = false + // Set the format of the captured raw audio data. + private val sampleRate = 16000 + private val numberOfChannels = 1 + private val samplesPerCall = 1024 + ``` + + + ```kotlin + // Set the format of the captured raw audio data. + private val sampleRate = 16000 + private val numberOfChannels = 1 + private val samplesPerCall = 1024 + ``` + + + + ```swift + var videoFrameDelegate: ModifyVideoFrameDelegate? + var audioFrameDelegate: ModifyAudioFrameDelegate? + ``` + + + - AgoraVideoFrameDelegate + - AgoraAudioFrameDelegate + + + - AgoraVideoFrameDelegate + - AgoraAudioFrameDelegate + + + + + ```csharp + internal byte[] VideoBuffer = new byte[0]; + private bool _needResize = false; + public int _videoFrameWidth = 1080; + public int VideoFrameWidth + { + set + { + if (value != _videoFrameWidth) + { + _needResize = true; + } + } + + get + { + return _videoFrameWidth; + } + } + + public int _videoFrameHeight = 720; + public int VideoFrameHeight + { + set + { + if (value != _videoFrameHeight) + { + _needResize = true; + } + } + + get + { + return _videoFrameHeight; + } + } + private bool _isTextureAttach = false; + private Texture2D _texture; + public int CHANNEL = 2; + public int PULL_FREQ_PER_SEC = 100; + public int SAMPLE_RATE = 48000; + internal int _count; + internal int _writeCount; + internal int _readCount; + internal RingBuffer _audioBuffer; + internal AudioClip _audioClip; + internal bool isPlaying = false; + ``` + + + ```csharp + private bool _isTextureAttach = false; + private Texture2D _texture; + public int CHANNEL = 2; + public int PULL_FREQ_PER_SEC = 100; + public int SAMPLE_RATE = 48000; + internal int _count; + internal int _writeCount; + internal int _readCount; + internal RingBuffer _audioBuffer; + internal AudioClip _audioClip; + internal bool isPlaying = false; + ``` + + \ No newline at end of file diff --git a/assets/code/video-sdk/raw-video-audio/set-video-frame-observer.mdx b/assets/code/video-sdk/raw-video-audio/set-video-frame-observer.mdx new file mode 100644 index 000000000..c7b6bb945 --- /dev/null +++ b/assets/code/video-sdk/raw-video-audio/set-video-frame-observer.mdx @@ -0,0 +1,104 @@ + + ```kotlin + private val iVideoFrameObserver: IVideoFrameObserver = object : IVideoFrameObserver { + override fun onCaptureVideoFrame(sourceType: Int, videoFrame: VideoFrame): Boolean { + modifyVideoBuffer(videoFrame) + return true + } + + override fun onPreEncodeVideoFrame(sourceType: Int, videoFrame: VideoFrame?): Boolean { + return false + } + + override fun onMediaPlayerVideoFrame(videoFrame: VideoFrame, i: Int): Boolean { + return false + } + + override fun onRenderVideoFrame(s: String, i: Int, videoFrame: VideoFrame): Boolean { + return true + } + + override fun getVideoFrameProcessMode(): Int { + // The process mode of the video frame. 0 means read-only, and 1 means read-and-write. + return 1 + } + + override fun getVideoFormatPreference(): Int { + return 1 + } + + override fun getRotationApplied(): Boolean { + return false + } + + override fun getMirrorApplied(): Boolean { + return false + } + + override fun getObservedFramePosition(): Int { + return 0 + } + } + ``` + - IVideoFrameObserver + + + ```swift + extension ModifyVideoFrameDelegate: AgoraVideoFrameDelegate { + public func onCapture( + _ videoFrame: AgoraOutputVideoFrame, sourceType: AgoraVideoSourceType + ) -> Bool { + // Change the video frame immediately after recording it + true + } + + // Indicate the video frame mode of the observer + public func getVideoFrameProcessMode() -> AgoraVideoFrameProcessMode { + // The process mode of the video frame: readOnly, readWrite + // Default is `.readOnly` function is required to change the output. + .readWrite + } + } + ``` + + + - AgoraVideoFrameDelegate + + + - AgoraVideoFrameDelegate + + + + ```csharp + // Internal class for handling media player events + internal class RawAudioVideoEventHandler : IVideoFrameObserver + { + private RawAudioVideoManager rawAudioVideoManager; + + internal RawAudioVideoEventHandler(RawAudioVideoManager refRawAudioVideoManager) + { + rawAudioVideoManager = refRawAudioVideoManager; + } + + public override bool OnCaptureVideoFrame(VIDEO_SOURCE_TYPE type, VideoFrame videoFrame) + { + rawAudioVideoManager.VideoFrameWidth = videoFrame.width; + rawAudioVideoManager.VideoFrameHeight = videoFrame.height; + lock (rawAudioVideoManager.VideoBuffer) + { + rawAudioVideoManager.VideoBuffer = videoFrame.yBuffer; + } + return true; + } + + public override bool OnRenderVideoFrame(string channelId, uint uid, VideoFrame videoFrame) + { + Debug.Log("OnRenderVideoFrameHandler-----------" + " uid:" + uid + " width:" + videoFrame.width + + " height:" + videoFrame.height); + return true; + } + } + ``` + - IVideoFrameObserver + + diff --git a/assets/code/video-sdk/raw-video-audio/swift/register-frame-observers.mdx b/assets/code/video-sdk/raw-video-audio/swift/register-frame-observers.mdx index c68e616ce..8abbc28f3 100644 --- a/assets/code/video-sdk/raw-video-audio/swift/register-frame-observers.mdx +++ b/assets/code/video-sdk/raw-video-audio/swift/register-frame-observers.mdx @@ -1,6 +1,6 @@ -``` swift +```swift agoraEngine.setAudioFrameDelegate(self) agoraEngine.setVideoFrameDelegate(self) @@ -22,7 +22,7 @@ agoraEngine.setMixedAudioFrameParametersWithSampleRate( -``` swift +```swift agoraEngine.setAudioFrameDelegate(self) // Set the format of the captured raw audio data. diff --git a/assets/code/video-sdk/raw-video-audio/swift/unregister-frame-observers.mdx b/assets/code/video-sdk/raw-video-audio/swift/unregister-frame-observers.mdx index dee28f9ea..7a276bc3c 100644 --- a/assets/code/video-sdk/raw-video-audio/swift/unregister-frame-observers.mdx +++ b/assets/code/video-sdk/raw-video-audio/swift/unregister-frame-observers.mdx @@ -1,13 +1,13 @@ -``` swift +```swift agoraEngine.setAudioFrameDelegate(nil) agoraEngine.setVideoFrameDelegate(nil) ``` -``` swift +```swift agoraEngine.setAudioFrameDelegate(nil) ``` diff --git a/assets/code/video-sdk/raw-video-audio/unregister-video-audio-frame-observers.mdx b/assets/code/video-sdk/raw-video-audio/unregister-video-audio-frame-observers.mdx new file mode 100644 index 000000000..4a4606363 --- /dev/null +++ b/assets/code/video-sdk/raw-video-audio/unregister-video-audio-frame-observers.mdx @@ -0,0 +1,57 @@ + + ```kotlin + override fun leaveChannel() { + agoraEngine!!.registerVideoFrameObserver(null) + agoraEngine!!.registerAudioFrameObserver(null) + + super.leaveChannel() + } + ``` + + + ```swift + agoraEngine.setAudioFrameDelegate(nil) + agoraEngine.setVideoFrameDelegate(nil) + ``` + + + - setVideoFrameDelegate + - setAudioFrameDelegate + + + - setVideoFrameDelegate + - setAudioFrameDelegate + + + + + ```csharp + public override void DestroyEngine() + { + if (agoraEngine == null) + { + return; + } + agoraEngine.UnRegisterVideoFrameObserver(); + agoraEngine.UnRegisterAudioFrameObserver(); + base.DestroyEngine(); + } + ``` + - UnRegisterVideoFrameObserver + - UnRegisterAudioFrameObserver + + + ```csharp + public override void DestroyEngine() + { + if (agoraEngine == null) + { + return; + } + agoraEngine.UnRegisterAudioFrameObserver(); + base.DestroyEngine(); + } + ``` + - UnRegisterAudioFrameObserver + + \ No newline at end of file diff --git a/assets/code/video-sdk/spatial-audio/import-library.mdx b/assets/code/video-sdk/spatial-audio/import-library.mdx new file mode 100644 index 000000000..393079017 --- /dev/null +++ b/assets/code/video-sdk/spatial-audio/import-library.mdx @@ -0,0 +1,35 @@ + + ```kotlin + import io.agora.spatialaudio.ILocalSpatialAudioEngine + import io.agora.spatialaudio.LocalSpatialAudioConfig + import io.agora.spatialaudio.RemoteVoicePositionInfo + ``` + + + ```swift + import AgoraRtcKit + ``` + + +```javascript +import AgoraManager from "../agora_manager/agora_manager.js"; +import AgoraRTC from "agora-rtc-sdk-ng"; +``` + + + ```typescript + import { useEffect, useRef, useState } from "react"; + import AgoraRTC, { IBufferSourceAudioTrack, UID } from "agora-rtc-sdk-ng"; + import { + SpatialAudioExtension, + SpatialAudioProcessor + } from "agora-extension-spatial-audio"; + import { + useConnectionState, + useRemoteUsers, + useRTCClient, + AgoraRTCProvider + } from "agora-rtc-react"; + import AuthenticationWorkflowManager from "../authentication-workflow/authenticationWorkflowManager"; + ``` + diff --git a/assets/code/video-sdk/spatial-audio/play-media.mdx b/assets/code/video-sdk/spatial-audio/play-media.mdx new file mode 100644 index 000000000..9d3995d3e --- /dev/null +++ b/assets/code/video-sdk/spatial-audio/play-media.mdx @@ -0,0 +1,49 @@ + +Add a method to allow playing media files with spatial audio effects: +```javascript +const playMediaWithSpatialAudio = async () => { + const processor = spatialAudioExtension.createProcessor(); + processors.set("media-player", processor); + + const track = await AgoraRTC.createBufferSourceAudioTrack({ + source: "./sample.mp3", + }); + + // Define the spatial position for the local audio player. + const mockLocalPlayerNewPosition = { + position: [0, 0, 0], + forward: [0, 0, 0], + }; + + // Update the spatial position for the local audio player. + processor.updatePlayerPositionInfo(mockLocalPlayerNewPosition); + + track.startProcessAudioBuffer({ loop: true }); + track.pipe(processor).pipe(track.processorDestination); + track.play(); + return track; +}; +``` + +You can call this method in the UI as follows: +```javascript +document.getElementById("playAudioFile").onclick = + async function localPlayerStart() { + if (isMediaPlaying) { + channelParameters.mediaPlayerTrack.stop(); + isMediaPlaying = false; + document.getElementById("playAudioFile").textContent = + "Play audio file"; + return; + } + + let track = agoraManager.playMediaWithSpatialAudio(); + console.log(track) + + isMediaPlaying = true; + document.getElementById("playAudioFile").textContent = + "Stop playing audio"; + channelParameters.mediaPlayerTrack = track; + }; +``` + diff --git a/assets/code/video-sdk/spatial-audio/remove-spatial.mdx b/assets/code/video-sdk/spatial-audio/remove-spatial.mdx new file mode 100644 index 000000000..1eea20390 --- /dev/null +++ b/assets/code/video-sdk/spatial-audio/remove-spatial.mdx @@ -0,0 +1,37 @@ + + ```csharp + public override void Leave() + { + if(localSpatial != null) + { + localSpatial.ClearRemotePositions(); + } + base.Leave(); + } + ``` + + - ClearRemotePositions + + + - ClearRemotePositions + + + + ```typescript + const cleanupFunction = () => { + try { + const disablePromises = Array.from(processors.current.values()).map(async (processor) => { + if (processor) { + await processor.disable(); + } + }); + + Promise.all(disablePromises).catch((reason) => console.log(reason)); + processors.current.clear(); + AgoraRTC.registerExtensions([]); + } catch (error) { + console.error("Error in cleanup:", error); + } + }; + ``` + \ No newline at end of file diff --git a/assets/code/video-sdk/spatial-audio/set-variables.mdx b/assets/code/video-sdk/spatial-audio/set-variables.mdx new file mode 100644 index 000000000..ba4296069 --- /dev/null +++ b/assets/code/video-sdk/spatial-audio/set-variables.mdx @@ -0,0 +1,58 @@ + + ```kotlin + // Instance of the spatial audio engine + private var spatialAudioEngine: ILocalSpatialAudioEngine? = null + ``` + - ILocalSpatialAudioEngine + + + ```swift + var localSpatial: AgoraLocalSpatialAudioKit! + ``` + + - AgoraLocalSpatialAudioKit + + + - AgoraLocalSpatialAudioKit + + + + + ```csharp + private ILocalSpatialAudioEngine localSpatial; + ``` + + +```javascript +var distance = 0; // Used to define and change the the spatial position +var isMediaPlaying = false; + +const processors = new Map(); +const spatialAudioExtension = new SpatialAudioExtension({ + assetsPath: "/node_modules/agora-extension-spatial-audio/external/", +}); + +const mockLocalUserNewPosition = { + // In a production app, the position can be generated by + // dragging the local user's avatar in a 3D scene. + position: [1, 1, 1], // Coordinates in the world coordinate system + forward: [1, 0, 0], // The unit vector of the front axis + right: [0, 1, 0], // The unit vector of the right axis + up: [0, 0, 1], // The unit vector of the vertical axis +}; +``` + + + ```typescript + const [isMediaPlaying, setMediaPlaying] = useState(false); + const [isRegistered, setRegistered] = useState(false); + const [audioFileTrack, setAudioFileTrack] = useState(null); + const remoteUsers = useRemoteUsers(); + const numberOfRemoteUsers = remoteUsers.length; + const remoteUser = remoteUsers[numberOfRemoteUsers - 1]; + const extension = useRef(null); + const processors = useRef>(new Map()); + const [distance, setDistance] = useState(0); + const mediaPlayerKey = "media-player"; + ``` + diff --git a/assets/code/video-sdk/spatial-audio/setup-local.mdx b/assets/code/video-sdk/spatial-audio/setup-local.mdx new file mode 100644 index 000000000..6ee629292 --- /dev/null +++ b/assets/code/video-sdk/spatial-audio/setup-local.mdx @@ -0,0 +1,98 @@ + + ```kotlin + // Define the position of the local user + val pos = floatArrayOf(0.0f, 0.0f, 0.0f) + val forward = floatArrayOf(1.0f, 0.0f, 0.0f) + val right = floatArrayOf(0.0f, 1.0f, 0.0f) + val up = floatArrayOf(0.0f, 0.0f, 1.0f) + // Set the position of the local user + spatialAudioEngine?.updateSelfPosition(pos, forward, right, up) + ``` + - ILocalSpatialAudioEngine.updateSelfPosition + + + ```swift + func updateLocalUser() { + // Self position at origin, x-right, y-up, facing -Z axis + let pos: [NSNumber] = [0, 0, 0] + let right: [NSNumber] = [1, 0, 0] + let up: [NSNumber] = [0, 1, 0] + let forward: [NSNumber] = [0, 0, -1] + + self.localSpatial.updateSelfPosition( + pos, + axisForward: forward, + axisRight: right, + axisUp: up + ) + } + ``` + + - updateSelfPosition(_:axisForward:axisRight:axisUp:) + + + - updateSelfPosition(_:axisForward:axisRight:axisUp:) + + + + + ```javascript + spatialAudioExtension.updateSelfPosition( + mockLocalUserNewPosition.position, + mockLocalUserNewPosition.forward, + mockLocalUserNewPosition.right, + mockLocalUserNewPosition.up + ); + ``` + - [updateSelfPosition](#updateselfposition) + + + 1. Process and play the audio file to test the local spatial audio features: + ```typescript + const AudioFileTrack: React.FC<{ track: IBufferSourceAudioTrack }> = ({ track }) => { + useEffect(() => { + track.startProcessAudioBuffer({ loop: true }); + track.play(); // to play the track for the local user + return () => { + track.stopProcessAudioBuffer(); + track.stop(); + }; + }, [track]); + return
Audio file is playing. Use +/- to change the spatial audio position
; + }; + const PlayMediaFile = () => { + const processor = processors.current.get(mediaPlayerKey); + if(!processor) + { + const processorRef = extension.current!.createProcessor(); + processors.current.set(mediaPlayerKey, processorRef); + AgoraRTC.createBufferSourceAudioTrack({ + source: "../src/assets/sample.wav", // Replace with the actual audio file path + }) + .then((track) => + { + track.pipe(processorRef).pipe(track.processorDestination); + setAudioFileTrack(track); + }) + .catch((error) => console.log(error)); + } + setMediaPlaying(!isMediaPlaying); + }; + ``` + - createBufferSourceAudioTrack + - BufferSourceAudioTrackInitConfig + - pipe + + 2. Update the spatial audio position of the audio file: + ```typescript + if (isMediaPlaying) { + // update the spatial position of the audio file. + const processorRef = processors.current.get(mediaPlayerKey); + processorRef?.updatePlayerPositionInfo({ + position: [distance, 0, 0], + forward: [1, 0, 0], + }); + } + ``` + +
diff --git a/assets/code/video-sdk/spatial-audio/setup-remote.mdx b/assets/code/video-sdk/spatial-audio/setup-remote.mdx new file mode 100644 index 000000000..2f7580a84 --- /dev/null +++ b/assets/code/video-sdk/spatial-audio/setup-remote.mdx @@ -0,0 +1,147 @@ + + ```kotlin + fun updateRemoteSpatialAudioPosition(remoteUid: Int, front: Float, right: Float, top: Float) { + // Define a remote user's spatial position + val positionInfo = RemoteVoicePositionInfo() + // The three values represent the front, right, and top coordinates + positionInfo.position = floatArrayOf(front, right, top) + positionInfo.forward = floatArrayOf(0.0f, 0.0f, -1.0f) + + // Update the spatial position of the specified remote user + spatialAudioEngine?.updateRemotePosition(remoteUid, positionInfo) + sendMessage("Spatial position of remote user ${remoteUid} updated") + } + ``` + - RemoteVoicePositionInfo + - ILocalSpatialAudioEngine.updateRemotePosition + + + ```swift + func updateRemoteUser(_ uid: UInt, position: [NSNumber], forward: [NSNumber]) { + let positionInfo = AgoraRemoteVoicePositionInfo() + positionInfo.position = position + positionInfo.forward = forward + + self.localSpatial.updateRemotePosition( + uid, positionInfo: positionInfo + ) + } + ``` + + + - AgoraRemoteVoicePositionInfo + - updateRemotePosition(_:positionInfo:) + + + - AgoraRemoteVoicePositionInfo + - updateRemotePosition(_:positionInfo:) + + + + ```csharp + public void UpdateSpatialAudioPosition(float sourceDistance) + { + if (remoteUid < 1) + { + Debug.Log("No remote user in the channel"); + return; + } + // Set the coordinates in the world coordinate system. + // This parameter is an array of length 3 + // The three values represent the front, right, and top coordinates + float[] position = new float[] { sourceDistance, 4.0F, 0.0F }; + // Set the unit vector of the x-axis in the coordinate system. + // This parameter is an array of length 3, + // The three values represent the front, right, and top coordinates + float[] forward = new float[] { sourceDistance, 0.0F, 0.0F }; + // Update the spatial position of the specified remote user + RemoteVoicePositionInfo remotePosInfo = new RemoteVoicePositionInfo(position, forward); + int res = localSpatial.UpdateRemotePosition((uint)remoteUid, remotePosInfo); + if (res == 0) + { + Debug.Log("Remote user spatial position updated"); + } + else + { + Debug.Log("Updating position failed with error: " + res); + } + } + ``` + + - RemoteVoicePositionInfo + - UpdateRemotePosition + + + - RemoteVoicePositionInfo + - UpdateRemotePosition + + + + + Add a method `updatePosition` to update remote positions: + + ```javascript + function updatePosition(distance, channelParameters) { + if (isMediaPlaying) { + const processor = processors.get("media-player"); + processor.updatePlayerPositionInfo({ + position: [distance, 0, 0], + forward: [1, 0, 0], + }); + } else { + const processor = processors.get(channelParameters.remoteUid); + processor.updateRemotePosition({ + position: [distance, 0, 0], + forward: [1, 0, 0], + }); + } + } + ``` + - [updateRemotePosition](#updateremoteposition) + + You can call `updatePosition` in the UI as follows: + + ```javascript + document.getElementById("decreaseDistance").onclick = async function () { + distance -= 5; + document.getElementById("distanceLabel").textContent = distance; + agoraManager.updatePosition(distance, channelParameters); + }; + + document.getElementById("increaseDistance").onclick = async function () { + distance += 5; + document.getElementById("distanceLabel").textContent = distance; + agoraManager.updatePosition(distance, channelParameters); + }; + ``` + + + 1. Create processors for each remote user: + ```typescript + if (remoteUser && !processors.current.has(remoteUser.uid)) { + console.log("Initializing spatial audio processor..."); + try { + const processor = extension.createProcessor(); + processors.current.set(remoteUser.uid, processor); + remoteUser.audioTrack?.pipe(processor).pipe(remoteUser.audioTrack.processorDestination); + await processor.enable(); + } catch (error) { + console.error("Error enabling spatial extension:", error); + } + } + ``` + - pipe + + 1. Update the spatial position of a remote user: + ```typescript + else if (remoteUser) { + // Update the spatial position of the remote user. + const processorRef = processors.current.get(remoteUser.uid); + processorRef?.updateRemotePosition({ + position: [distance, 0, 0], + forward: [1, 0, 0], + }); + } + ``` + + diff --git a/assets/code/video-sdk/spatial-audio/setup-spatial.mdx b/assets/code/video-sdk/spatial-audio/setup-spatial.mdx new file mode 100644 index 000000000..0ac3d70cb --- /dev/null +++ b/assets/code/video-sdk/spatial-audio/setup-spatial.mdx @@ -0,0 +1,151 @@ + + ```kotlin + private fun configureSpatialAudioEngine() { + // Enable spatial audio + agoraEngine!!.enableSpatialAudio(true) + + // Create and initialize the spatial audio engine + val localSpatialAudioConfig = LocalSpatialAudioConfig() + localSpatialAudioConfig.mRtcEngine = agoraEngine + spatialAudioEngine = ILocalSpatialAudioEngine.create() + spatialAudioEngine?.initialize(localSpatialAudioConfig) + + // Set the audio reception range of the local user in meters + spatialAudioEngine?.setAudioRecvRange(50F) + + // Set the length of unit distance in meters + spatialAudioEngine?.setDistanceUnit(1F) + } + ``` + - enableSpatialAudio + - LocalSpatialAudioConfig + - ILocalSpatialAudioEngine.create + - ILocalSpatialAudioEngine.initialize + - ILocalSpatialAudioEngine.setAudioRecvRange + - ILocalSpatialAudioEngine.setDistanceUnit + + + + ```swift + func configureSpatialAudioEngine() { + agoraEngine.setAudioProfile(.speechStandard, scenario: .gameStreaming) + + // The next line is only required if using bluetooth headphones from iOS/iPadOS + agoraEngine.setParameters(#"{"che.audio.force_bluetooth_a2dp":true}"#) + + agoraEngine.enableSpatialAudio(true) + let localSpatialAudioConfig = AgoraLocalSpatialAudioConfig() + localSpatialAudioConfig.rtcEngine = agoraEngine + localSpatial = AgoraLocalSpatialAudioKit.sharedLocalSpatialAudio(with: localSpatialAudioConfig) + + // By default Agora subscribes to the audio streams of all remote users. + // Unsubscribe all remote users; otherwise, the audio reception range you set + // is invalid. + localSpatial.muteLocalAudioStream(false) + localSpatial.muteAllRemoteAudioStreams(false) + + // Set the audio reception range, in meters, of the local user + localSpatial.setAudioRecvRange(50) + // Set the length, in meters, of unit distance + localSpatial.setDistanceUnit(1) + } + ``` + + + - setAudioProfile(_:scenario:) + - enableSpatialAudio(_:) + - AgoraLocalSpatialAudioConfig + - sharedLocalSpatialAudio(with:) + - muteLocalAudioStream(_:) + - muteAllRemoteAudioStreams(_:) + - setAudioRecvRange(_:) + - setDistanceUnit(_:) + + + - setAudioProfile(_:scenario:) + - enableSpatialAudio(_:) + - AgoraLocalSpatialAudioConfig + - sharedLocalSpatialAudio(with:) + - muteLocalAudioStream(_:) + - muteAllRemoteAudioStreams(_:) + - setAudioRecvRange(_:) + - setDistanceUnit(_:) + + + + + ```csharp + private void ConfigureSpatialAudioEngine() + { + agoraEngine.EnableSpatialAudio(true); + LocalSpatialAudioConfig localSpatialAudioConfig = new LocalSpatialAudioConfig(); + localSpatialAudioConfig.rtcEngine = agoraEngine; + localSpatial = agoraEngine.GetLocalSpatialAudioEngine(); + localSpatial.Initialize(); + // By default Agora subscribes to the audio streams of all remote users. + // Unsubscribe all remote users; otherwise, the audio reception range you set + // is invalid. + localSpatial.MuteLocalAudioStream(true); + localSpatial.MuteAllRemoteAudioStreams(true); + + // Set the audio reception range, in meters, of the local user + localSpatial.SetAudioRecvRange(50); + + // Set the length, in meters, of unit distance + localSpatial.SetDistanceUnit(1); + + // Update self position + float[] pos = new float[] { 0.0F, 0.0F, 0.0F }; + float[] forward = new float[] { 1.0F, 0.0F, 0.0F }; + float[] right = new float[] { 0.0F, 1.0F, 0.0F }; + float[] up = new float[] { 0.0F, 0.0F, 1.0F }; + // Set the position of the local user + localSpatial.UpdateSelfPosition(pos, forward, right, up); + } + ``` + + - GetLocalSpatialAudioEngine + - LocalSpatialAudioConfig + - Initialize + - MuteLocalAudioStream + - MuteAllRemoteAudioStreams + - SetAudioRecvRange + - SetDistanceUnit + - UpdateSelfPosition + + + - GetLocalSpatialAudioEngine + - LocalSpatialAudioConfig + - Initialize + - MuteLocalAudioStream + - MuteAllRemoteAudioStreams + - SetAudioRecvRange + - SetDistanceUnit + - UpdateSelfPosition + + + +```javascript +// Enable spatial audio +AgoraRTC.registerExtensions([spatialAudioExtension]); +``` + + + 1. Create an instance of the spatial audio engine: + ```typescript + const extension = new SpatialAudioExtension({ + assetsPath: "./node_modules/agora-extension-spatial-audio/external/", + }); + ``` + 2. Register the extension with the engine: + ```typescript + const initializeSpatialProcessor = async () => { + if(!isRegistered) + { + console.log("Registering spatial audio extension..."); + AgoraRTC.registerExtensions([extension]); + setRegistered(true); + } + }; + ``` + diff --git a/assets/code/video-sdk/virtual-background/blur-background.mdx b/assets/code/video-sdk/virtual-background/blur-background.mdx new file mode 100644 index 000000000..b1f3ca434 --- /dev/null +++ b/assets/code/video-sdk/virtual-background/blur-background.mdx @@ -0,0 +1,75 @@ + + ```kotlin + fun setBlurBackground() { + val virtualBackgroundSource = VirtualBackgroundSource() + virtualBackgroundSource.backgroundSourceType = VirtualBackgroundSource.BACKGROUND_BLUR + virtualBackgroundSource.blurDegree = VirtualBackgroundSource.BLUR_DEGREE_MEDIUM + setBackground(virtualBackgroundSource) + } + + private fun setBackground(virtualBackgroundSource: VirtualBackgroundSource) { + // Set processing properties for background + val segmentationProperty = SegmentationProperty() + segmentationProperty.modelType = SegmentationProperty.SEG_MODEL_AI + // Use SEG_MODEL_GREEN if you have a green background + segmentationProperty.greenCapacity = + 0.5f // Accuracy for identifying green colors (range 0-1) + + // Enable or disable virtual background + agoraEngine!!.enableVirtualBackground( + true, + virtualBackgroundSource, segmentationProperty + ) + } + ``` + - VirtualBackgroundSource + - SegmentationProperty + - enableVirtualBackground + + + + ```swift + func blurBackground() { + let virtualBackgroundSource = AgoraVirtualBackgroundSource() + virtualBackgroundSource.backgroundSourceType = .blur + virtualBackgroundSource.blurDegree = .high + + let segData = AgoraSegmentationProperty() + segData.modelType = .agoraAi + + agoraEngine.enableVirtualBackground(true, backData: virtualBackgroundSource, segData: segData) + } + ``` + + + - AgoraVirtualBackgroundSource + - AgoraSegmentationProperty + - enableVirtualBackground + + + - AgoraVirtualBackgroundSource + - AgoraSegmentationProperty + - enableVirtualBackground + + + + ```typescript + const blurBackground = () => { + processor.current?.setOptions({ type: "blur", blurDegree: 2 }); + }; + ``` + + +```js +// Blur the user's actual background +async function setBackgroundBlurring(channelParameters) { + if (channelParameters.localVideoTrack) { + let processor = await getProcessorInstance(channelParameters); + processor.setOptions({ type: "blur", blurDegree: 2 }); + await processor.enable(); + + isVirtualBackGroundEnabled = true; + } +} +``` + diff --git a/assets/code/video-sdk/virtual-background/color-background.mdx b/assets/code/video-sdk/virtual-background/color-background.mdx new file mode 100644 index 000000000..fd00c7ca0 --- /dev/null +++ b/assets/code/video-sdk/virtual-background/color-background.mdx @@ -0,0 +1,101 @@ + + ```kotlin + fun setSolidBackground() { + val virtualBackgroundSource = VirtualBackgroundSource() + virtualBackgroundSource.backgroundSourceType = VirtualBackgroundSource.BACKGROUND_COLOR + virtualBackgroundSource.color = 0x0000FF + setBackground(virtualBackgroundSource) + } + ``` + - VirtualBackgroundSource + + + ```swift + func colorBackground() { + let virtualBackgroundSource = AgoraVirtualBackgroundSource() + virtualBackgroundSource.backgroundSourceType = .color + virtualBackgroundSource.color = convertColorToHex(.red) + + let segData = AgoraSegmentationProperty() + segData.modelType = .agoraAi + + agoraEngine.enableVirtualBackground(true, backData: virtualBackgroundSource, segData: segData) + } + ``` + + For converting the color to a hex Integer: + + + ```swift + func convertColorToHex(_ color: NSColor) -> UInt { + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + + color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + + let redInt = UInt(red * 255) + let greenInt = UInt(green * 255) + let blueInt = UInt(blue * 255) + + let hexValue = (redInt << 16) | (greenInt << 8) | blueInt + + return hexValue + } + ``` + + + ```swift + func convertColorToHex(_ color: UIColor) -> UInt { + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + + color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + + let redInt = UInt(red * 255) + let greenInt = UInt(green * 255) + let blueInt = UInt(blue * 255) + + let hexValue = (redInt << 16) | (greenInt << 8) | blueInt + + return hexValue + } + ``` + + + + - AgoraVirtualBackgroundSource + - AgoraSegmentationProperty + - enableVirtualBackground + + + - AgoraVirtualBackgroundSource + - AgoraSegmentationProperty + - enableVirtualBackground + + + + + ```typescript + const colorBackground = () => { + processor.current?.setOptions({ type: "color", color: "#00ff00" }); + }; + ``` + + +```js +// Set a solid color as the background +async function setBackgroundColor(channelParameters) { + if (channelParameters.localVideoTrack) { + let processor = await getProcessorInstance(channelParameters); + processor.setOptions({ type: "color", color: "#00ff00" }); + await processor.enable(); + + isVirtualBackGroundEnabled = true; + } +} +``` + diff --git a/assets/code/video-sdk/virtual-background/configure-engine.mdx b/assets/code/video-sdk/virtual-background/configure-engine.mdx new file mode 100644 index 000000000..c0ec078e5 --- /dev/null +++ b/assets/code/video-sdk/virtual-background/configure-engine.mdx @@ -0,0 +1,26 @@ + +```typescript +function VirtualBackground() { + const agoraEngine = useRTCClient(AgoraRTC.createClient({ codec: "vp8", mode: config.selectedProduct })); + + return ( +
+

Virtual Background

+ + + + + +
+ ); +} +``` + - useRTCClient + - AgoraRTCProvider + +
+ +```js +const agoraManager = await AgoraManager(eventsCallback); +``` + diff --git a/assets/code/video-sdk/virtual-background/device-compatibility.mdx b/assets/code/video-sdk/virtual-background/device-compatibility.mdx new file mode 100644 index 000000000..672c068dd --- /dev/null +++ b/assets/code/video-sdk/virtual-background/device-compatibility.mdx @@ -0,0 +1,48 @@ + + ```kotlin + fun isFeatureAvailable() :Boolean { + return agoraEngine!!.isFeatureAvailableOnDevice( + Constants.FEATURE_VIDEO_VIRTUAL_BACKGROUND + ) + } + ``` + - isFeatureAvailableOnDevice + + + ```swift + guard agoraEngine.isFeatureAvailable(onDevice: .videoPreprocessVirtualBackground) else { + // Device doesn't support virtual background + return + } + ``` + + + - isFeatureAvailable + + + - isFeatureAvailable + + + + + ```typescript + const checkCompatibility = () => { + if (!extension.current.checkCompatibility()) { + console.error("Does not support virtual background!"); + return; + } + } + ``` + + +Add the following code after `const extension = new VirtualBackgroundExtension();`: +```js +// Create a VirtualBackgroundExtension instance + const extension = new VirtualBackgroundExtension(); + // Check browser compatibility virtual background extension + if (!extension.checkCompatibility()) { + console.error("Does not support Virtual Background!"); + // Handle exit code + } +``` + diff --git a/assets/code/video-sdk/virtual-background/image-background.mdx b/assets/code/video-sdk/virtual-background/image-background.mdx new file mode 100644 index 000000000..90105d8d2 --- /dev/null +++ b/assets/code/video-sdk/virtual-background/image-background.mdx @@ -0,0 +1,66 @@ + + ```kotlin + fun setImageBackground() { + val virtualBackgroundSource = VirtualBackgroundSource() + virtualBackgroundSource.backgroundSourceType = VirtualBackgroundSource.BACKGROUND_IMG + virtualBackgroundSource.source = "" + setBackground(virtualBackgroundSource) + } + ``` + - VirtualBackgroundSource + + + + For this example, you should include an image `"background_ss.jpg"` in your App's bundle. + + ```swift + func imageBackground() { + let virtualBackgroundSource = AgoraVirtualBackgroundSource() + virtualBackgroundSource.backgroundSourceType = .img + virtualBackgroundSource.source = Bundle.main.path(forResource: "background_ss", ofType: "jpg") + + let segData = AgoraSegmentationProperty() + segData.modelType = .agoraAi + + agoraEngine.enableVirtualBackground(true, backData: virtualBackgroundSource, segData: segData) + } + ``` + + - AgoraVirtualBackgroundSource + - AgoraSegmentationProperty + - enableVirtualBackground + + + - AgoraVirtualBackgroundSource + - AgoraSegmentationProperty + - enableVirtualBackground + + + + ```typescript + const imageBackground = () => { + const image = new Image(); + image.onload = () => { + processor.current?.setOptions({ type: "img", source: image }); + }; + image.src = demoImage; + }; + ``` + + +```js +// Set an image as the background +async function setBackgroundImage(channelParameters) { + const imgElement = document.createElement("img"); + + imgElement.onload = async () => { + let processor = await getProcessorInstance(channelParameters); + processor.setOptions({ type: "img", source: imgElement }); + await processor.enable(); + + isVirtualBackGroundEnabled = true; + }; + imgElement.src = "./background.jpg"; +} +``` + diff --git a/assets/code/video-sdk/virtual-background/import-library.mdx b/assets/code/video-sdk/virtual-background/import-library.mdx new file mode 100644 index 000000000..4b4cb3536 --- /dev/null +++ b/assets/code/video-sdk/virtual-background/import-library.mdx @@ -0,0 +1,61 @@ + + ```kotlin + import io.agora.rtc2.Constants + import io.agora.rtc2.video.SegmentationProperty + import io.agora.rtc2.video.VirtualBackgroundSource + ``` + + + + + ```swift + import AgoraRtcKit + ``` + + --- + + You must also import the virtual background plugin to your target. + + i. **Swift Package Manager** + + Add the product "VirtualBackground" to your app target. This is part of the AgoraRtcEngine Swift Package. + + ii. **CocoaPods** + + Include "VirtualBackground" in the subspecs in your Podfile: + + + ```rb + target 'Your App' do + pod 'AgoraRtcEngine_iOS', '~> 4.2', :subspecs => ['RtcBasic', 'VirtualBackground'] + end + ``` + + + ```rb + target 'Your App' do + pod 'AgoraRtcEngine_macOS', '~> 4.2', :subspecs => ['RtcBasic', 'VirtualBackground'] + end + ``` + + + --- + + +```typescript +import AgoraRTC from "agora-rtc-sdk-ng"; +import AuthenticationWorkflowManager from "../authentication-workflow/authenticationWorkflowManager"; +import VirtualBackgroundExtension, { IVirtualBackgroundProcessor } from "agora-extension-virtual-background"; +import { useConnectionState } from 'agora-rtc-react'; +import { useAgoraContext } from "../agora-manager/agoraManager"; +import wasm from "agora-extension-virtual-background/wasms/agora-wasm.wasm?url"; +import demoImage from '../assets/image.webp'; +``` + + +```js +import AgoraManager from "../agora_manager/agora_manager.js"; +import AgoraRTC from "agora-rtc-sdk-ng"; +import VirtualBackgroundExtension from "agora-extension-virtual-background"; +``` + diff --git a/assets/code/video-sdk/virtual-background/reset-background.mdx b/assets/code/video-sdk/virtual-background/reset-background.mdx new file mode 100644 index 000000000..f8b0fb5d8 --- /dev/null +++ b/assets/code/video-sdk/virtual-background/reset-background.mdx @@ -0,0 +1,60 @@ + + ```kotlin + fun removeBackground() { + // Disable virtual background + agoraEngine!!.enableVirtualBackground( + false, + VirtualBackgroundSource(), SegmentationProperty() + ) + } + ``` + + + ```swift + agoraEngine.enableVirtualBackground(false, backData: nil, segData: nil) + ``` + + + - enableVirtualBackground + + + - enableVirtualBackground + + + + +```typescript +function VirtualBackgroundComponent() { + const [isVirtualBackground, setVirtualBackground] = useState(false); + const connectionState = useConnectionState(); + + return ( +
+ {isVirtualBackground ? ( +
+ + +
+ ) : ( + + )} +
+ ); +} +``` + - useConnectionState + +
+ +```js +// Disable background +async function disableBackground(channelParameters) { + let processor = await getProcessorInstance(channelParameters); + processor.disable(); + + isVirtualBackGroundEnabled = false; +} +``` + diff --git a/assets/code/video-sdk/virtual-background/set-virtual-background.mdx b/assets/code/video-sdk/virtual-background/set-virtual-background.mdx new file mode 100644 index 000000000..b5708733b --- /dev/null +++ b/assets/code/video-sdk/virtual-background/set-virtual-background.mdx @@ -0,0 +1,115 @@ + + ```csharp + public void setVirtualBackground(bool enableVirtualBackgroud, string option) + { + if(agoraEngine == null) + { + Debug.Log("Please join a channel to enable virtual background"); + return; + } + VirtualBackgroundSource virtualBackgroundSource = new VirtualBackgroundSource(); + + // Set the type of virtual background + if (option == "Blur") + { // Set background blur + virtualBackgroundSource.background_source_type = BACKGROUND_SOURCE_TYPE.BACKGROUND_BLUR; + virtualBackgroundSource.blur_degree = BACKGROUND_BLUR_DEGREE.BLUR_DEGREE_HIGH; + Debug.Log("Blur background enabled"); + } + else if (option == "Color") + { // Set a solid background color + virtualBackgroundSource.background_source_type = BACKGROUND_SOURCE_TYPE.BACKGROUND_COLOR; + virtualBackgroundSource.color = 0x0000FF; + Debug.Log("Color background enabled"); + } + else if (option == "Image") + { // Set a background image + virtualBackgroundSource.background_source_type = BACKGROUND_SOURCE_TYPE.BACKGROUND_IMG; + virtualBackgroundSource.source = "Assets/Resources/agora.png"; + Debug.Log("Image background enabled"); + } + + // Set processing properties for background + SegmentationProperty segmentationProperty = new SegmentationProperty(); + segmentationProperty.modelType = SEG_MODEL_TYPE.SEG_MODEL_AI; // Use SEG_MODEL_GREEN if you have a green background + segmentationProperty.greenCapacity = 0.5F; // Accuracy for identifying green colors (range 0-1) + + // Enable or disable virtual background + agoraEngine.EnableVirtualBackground(enableVirtualBackgroud, virtualBackgroundSource, segmentationProperty); + } + ``` + - VirtualBackgroundSource + - EnableVirtualBackground + - SegmentationProperty + + + +```typescript +useEffect(() => { + const initializeVirtualBackgroundProcessor = async () => { + AgoraRTC.registerExtensions([extension.current]); + + checkCompatibility(); + + if (agoraContext.localCameraTrack) { + console.log("Initializing virtual background processor..."); + try { + processor.current = extension.current.createProcessor(); + await processor.current.init(wasm); + agoraContext.localCameraTrack.pipe(processor.current).pipe(agoraContext.localCameraTrack.processorDestination); + processor.current.setOptions({ type: "color", color: "#00ff00" }); + await processor.current.enable(); + setSelectedOption('color'); + } catch (error) { + console.error("Error initializing virtual background:", error); + } + } + }; + + void initializeVirtualBackgroundProcessor(); + + return () => { + const disableVirtualBackground = async () => { + processor.current?.unpipe(); + agoraContext.localCameraTrack?.unpipe(); + await processor.current?.disable(); + }; + void disableVirtualBackground(); + }; + }, [agoraContext.localCameraTrack]); +``` + - pipe + - unpipe + + + +```js + // Create a VirtualBackgroundExtension instance + const extension = new VirtualBackgroundExtension(); + + // Register the extension + AgoraRTC.registerExtensions([extension]); + let processor = null; + + // Initialization + async function getProcessorInstance(channelParameters) { + if (!processor && channelParameters.localVideoTrack) { + // Create a VirtualBackgroundProcessor instance + processor = extension.createProcessor(); + + try { + // Initialize the extension and pass in the URL of the Wasm file + await processor.init("./assets/wasms"); + } catch (e) { + console.log("Fail to load WASM resource!"); + return null; + } + // Inject the extension into the video processing pipeline in the SDK + channelParameters.localVideoTrack + .pipe(processor) + .pipe(channelParameters.localVideoTrack.processorDestination); + } + return processor; + } +``` + diff --git a/assets/code/voice-sdk/get-started-sdk/swift/create-ui.mdx b/assets/code/voice-sdk/get-started-sdk/swift/create-ui.mdx index ed008c1e4..59158e8ac 100644 --- a/assets/code/voice-sdk/get-started-sdk/swift/create-ui.mdx +++ b/assets/code/voice-sdk/get-started-sdk/swift/create-ui.mdx @@ -1,5 +1,5 @@ -``` swift +```swift import Cocoa import AppKit import Foundation @@ -47,7 +47,7 @@ class ViewController: NSViewController { - ``` swift + ```swift import UIKit import AVFoundation diff --git a/assets/code/voice-sdk/get-started-sdk/swift/show-message.mdx b/assets/code/voice-sdk/get-started-sdk/swift/show-message.mdx index fc02562b3..6aba83486 100644 --- a/assets/code/voice-sdk/get-started-sdk/swift/show-message.mdx +++ b/assets/code/voice-sdk/get-started-sdk/swift/show-message.mdx @@ -1,5 +1,5 @@ -``` swift +```swift func showMessage(title: String, text: String, delay: Int = 2) -> Void { let deadlineTime = DispatchTime.now() + .seconds(delay) DispatchQueue.main.asyncAfter(deadline: deadlineTime, execute: { @@ -13,7 +13,7 @@ func showMessage(title: String, text: String, delay: Int = 2) -> Void { ``` -``` swift +```swift func showMessage(title: String, text: String, delay: Int = 2) -> Void { let deadlineTime = DispatchTime.now() + .seconds(delay) DispatchQueue.main.asyncAfter(deadline: deadlineTime, execute: { diff --git a/assets/code/voice-sdk/get-started-sdk/swift/view-did-disappear.mdx b/assets/code/voice-sdk/get-started-sdk/swift/view-did-disappear.mdx index f1055a74e..9ec4b7c43 100644 --- a/assets/code/voice-sdk/get-started-sdk/swift/view-did-disappear.mdx +++ b/assets/code/voice-sdk/get-started-sdk/swift/view-did-disappear.mdx @@ -1,5 +1,5 @@ -``` swift +```swift override func viewDidDisappear() { super.viewDidDisappear() @@ -9,7 +9,7 @@ ``` -``` swift +```swift override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) leaveChannel() diff --git a/assets/images/chat/chat-call-logic-android.svg b/assets/images/chat/chat-call-logic-android.svg index 39eff30bf..8da563373 100644 --- a/assets/images/chat/chat-call-logic-android.svg +++ b/assets/images/chat/chat-call-logic-android.svg @@ -1 +1 @@ -Your appAgoraUserUserChat SDKChat SDKChatServerChatServerInitializeOpen appGet a ChatClient instanceagoraChatClient = ChatClient.getInstance()Initialize the instance:agoraChatClient.init(context, options)Add message event callbacks:agoraChatClient.chatManager().addMessageListener(...)Add connection event callbacks:agoraChatClient.addConnectionListener(...)Log inJoin a chatRetrieve authentication token for the userLog in to the chat server:agoraChatClient.loginWithAgoraToken(...)onConnected() callbackSend messagesSend a messageCreate a ChatMessageSend the ChatMessage:agoraChatClient.chatManager().sendMessage(message)Receive messagesonMessageReceived(messages) callbackDisplay messageCloseLeave the chatLog out:agoraChatClient.logout(...) \ No newline at end of file +Your appAgoraUserUserChat SDKChat SDKChatServerChatServerInitializeOpen appGet a ChatClient instanceagoraChatClient = ChatClient.getInstance()Initialize the instance:agoraChatClient.init(context, options)Add message event callbacks:agoraChatClient.chatManager().addMessageListener(...)Add connection event callbacks:agoraChatClient.addConnectionListener(...)Log inJoin a chatRetrieve authentication token for the userLog in to the chat server:agoraChatClient.loginWithAgoraToken(...)onConnected() callbackSend messagesSend a messageCreate a ChatMessageSend the ChatMessage:agoraChatClient.chatManager().sendMessage(message)Receive messagesonMessageReceived(messages) callbackDisplay messageCloseLeave the chatLog out:agoraChatClient.logout(...) \ No newline at end of file diff --git a/assets/images/chat/chat-call-logic-flutter.svg b/assets/images/chat/chat-call-logic-flutter.svg index 8753c4cf9..1d3d56b62 100644 --- a/assets/images/chat/chat-call-logic-flutter.svg +++ b/assets/images/chat/chat-call-logic-flutter.svg @@ -1 +1 @@ -Your appAgoraUserUserChat SDKChat SDKChatServerChatServerInitializeOpen appGet a ChatClient instanceagoraChatClient = ChatClient.getInstanceInitialize the instance:agoraChatClient.init(options)Add message event callbacks:agoraChatClient.chatManager.addEventHandler(...)Add connection event callbacks:agoraChatClient.addConnectionEventHandler(...)Log inJoin a chatRetrieve authentication token for the userLog in to the chat server:agoraChatClient.loginWithAgoraToken(userId, token)onConnected() callbackSend messagesSend a messageCreate a ChatMessageSend the ChatMessage:agoraChatClient.chatManager.sendMessage(message)Receive messagesonMessagesReceived(List<ChatMessage> messages) callbackDisplay messageCloseLeave the chatLog out:agoraChatClient.logout(...) \ No newline at end of file +Your appAgoraUserUserChat SDKChat SDKChatServerChatServerInitializeOpen appGet a ChatClient instanceagoraChatClient = ChatClient.getInstanceInitialize the instance:agoraChatClient.init(options)Add message event callbacks:agoraChatClient.chatManager.addEventHandler(...)Add connection event callbacks:agoraChatClient.addConnectionEventHandler(...)Log inJoin a chatRetrieve authentication token for the userLog in to the chat server:agoraChatClient.loginWithAgoraToken(userId, token)onConnected() callbackSend messagesSend a messageCreate a ChatMessageSend the ChatMessage:agoraChatClient.chatManager.sendMessage(message)Receive messagesonMessagesReceived(List<ChatMessage> messages) callbackDisplay messageCloseLeave the chatLog out:agoraChatClient.logout(...) \ No newline at end of file diff --git a/assets/images/chat/chat-call-logic-unity.svg b/assets/images/chat/chat-call-logic-unity.svg index 2cb29b852..4e7cb397f 100644 --- a/assets/images/chat/chat-call-logic-unity.svg +++ b/assets/images/chat/chat-call-logic-unity.svg @@ -1 +1 @@ -Your appAgoraUserUserChat SDKChat SDKChatServerChatServerInitializeOpen appInit a chat SDK instance:SDKClient.Instance.InitWithOptions(options);Add message event callbacks:SDKClient.Instance.ChatManager.AddChatManagerDelegate(this);Log inJoin a chatRetrieve authentication token for the userLog in to the chat server:SDKClient.Instance.LoginWithAgoraToken(...)onConnected() callbackSend messagesSend a messageCreate a ChatMessageSend the ChatMessage: SDKClient.Instance.ChatManager.SendMessage(...)Receive messagesonMessageReceived(messages) callbackDisplay messageCloseLeave the chatLog out:agoraChatClient.Logout(...) \ No newline at end of file +Your appAgoraUserUserChat SDKChat SDKChatServerChatServerInitializeOpen appInit a chat SDK instance:SDKClient.Instance.InitWithOptions(options);Add message event callbacks:SDKClient.Instance.ChatManager.AddChatManagerDelegate(this);Log inJoin a chatRetrieve authentication token for the userLog in to the chat server:SDKClient.Instance.LoginWithAgoraToken(...)onConnected() callbackSend messagesSend a messageCreate a ChatMessageSend the ChatMessage: SDKClient.Instance.ChatManager.SendMessage(...)Receive messagesonMessageReceived(messages) callbackDisplay messageCloseLeave the chatLog out:agoraChatClient.Logout(...) \ No newline at end of file diff --git a/assets/images/chat/chat-call-logic-windows.svg b/assets/images/chat/chat-call-logic-windows.svg index 57b2508b8..9e6776848 100644 --- a/assets/images/chat/chat-call-logic-windows.svg +++ b/assets/images/chat/chat-call-logic-windows.svg @@ -1 +1 @@ -Your appAgoraUserUserChat SDKChat SDKAgoraChatAgoraChatInitializeOpen appGet a AgoraChat SDKClient instanceSDKClient.InstanceInitialize the instance:SDKClient.Instance.InitWithOptions(options)Add message event callbacks:SDKClient.Instance.ChatManager.AddChatManagerDelegate(...)Add connection event callbacks:SDKClient.Instance.AddConnectionDelegate(...)Log inJoin a chatRetrieve authentication token for the userLog in to Agora Chat:SDKClient.Instance.LoginWithAgoraToken(...)onConnected() callbackSend messagesSend a messageCreate a ChatMessageSend the ChatMessage:SDKClient.Instance.ChatManager.SendMessage(...)Receive messagesonMessageReceived(messages) callbackDisplay messageCloseLeave the chatLog out:SDKClient.Instance.Logout(...) \ No newline at end of file +Your appAgoraUserUserChat SDKChat SDKAgoraChatAgoraChatInitializeOpen appGet a AgoraChat SDKClient instanceSDKClient.InstanceInitialize the instance:SDKClient.Instance.InitWithOptions(options)Add message event callbacks:SDKClient.Instance.ChatManager.AddChatManagerDelegate(...)Add connection event callbacks:SDKClient.Instance.AddConnectionDelegate(...)Log inJoin a chatRetrieve authentication token for the userLog in to Agora Chat:SDKClient.Instance.LoginWithAgoraToken(...)onConnected() callbackSend messagesSend a messageCreate a ChatMessageSend the ChatMessage:SDKClient.Instance.ChatManager.SendMessage(...)Receive messagesonMessageReceived(messages) callbackDisplay messageCloseLeave the chatLog out:SDKClient.Instance.Logout(...) \ No newline at end of file diff --git a/assets/images/extensions-marketplace/active-fence-add-an-editable-field.png b/assets/images/extensions-marketplace/active-fence-add-an-editable-field.png new file mode 100644 index 0000000000000000000000000000000000000000..925ef8dac28d5518e5ba0777b79ec5dc7593ed45 GIT binary patch literal 88758 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%0AJB#$m=KQ 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 Hu0mjfwG(Px literal 0 HcmV?d00001 diff --git a/assets/images/extensions-marketplace/active-fence.puml b/assets/images/extensions-marketplace/active-fence.puml index 14537c3c5..602fd41d4 100644 --- a/assets/images/extensions-marketplace/active-fence.puml +++ b/assets/images/extensions-marketplace/active-fence.puml @@ -1,4 +1,4 @@ -@startuml ncs-worflow +@startuml !include ../video-sdk/agora_skin.iuml actor "User" as USR diff --git a/assets/images/extensions-marketplace/active-fence.svg b/assets/images/extensions-marketplace/active-fence.svg index a9a5ad9f4..949ba5855 100644 --- a/assets/images/extensions-marketplace/active-fence.svg +++ b/assets/images/extensions-marketplace/active-fence.svg @@ -1 +1 @@ -Implemented by youProvided by AgoraProvided by ActiveFenceUserUserWeb serverWeb serverAppAppSD-RTNSD-RTNActiveFenceActiveFenceLoginLogin authenticationJoin channelJoin channel withActiveFence activatedMonitor contentin channelContent matchesworkflow riskAction webhookAct on webhook. For example, useChannel Management REST APIto remove user from channelLog user outLogout \ No newline at end of file +Implemented by youProvided by AgoraProvided by ActiveFenceUserUserWeb serverWeb serverAppAppSD-RTNSD-RTNActiveFenceActiveFenceLoginLogin authenticationJoin channelJoin channel withActiveFence activatedMonitor contentin channelContent matchesworkflow riskAction webhookAct on webhook. For example, useChannel Management REST APIto remove user from channelLog user outLogout \ No newline at end of file diff --git a/assets/images/extensions-marketplace/geofencing.svg b/assets/images/extensions-marketplace/geofencing.svg new file mode 100644 index 000000000..6aa7d43e0 --- /dev/null +++ b/assets/images/extensions-marketplace/geofencing.svg @@ -0,0 +1 @@ +Audio and video dataCapturePre-processEncodeTransmitDecodePost-processPlayAI Noise suppressionAudio data \ No newline at end of file diff --git a/assets/images/extensions-marketplace/livedata-ios-build-settings-add-objc.png b/assets/images/extensions-marketplace/livedata-ios-build-settings-add-objc.png new file mode 100755 index 0000000000000000000000000000000000000000..95a279c5041e10a0a216e27b7743f9660cf3d40b GIT binary patch literal 165224 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%0AMweFDdz{ZL-9g*n$Ie>l8BQ%3x(CT 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 literal 0 HcmV?d00001 diff --git a/assets/images/extensions-marketplace/livedata-ios-build-settings-capitalize-o-and-c.png b/assets/images/extensions-marketplace/livedata-ios-build-settings-capitalize-o-and-c.png new file mode 100644 index 0000000000000000000000000000000000000000..226eea019a56584a0f5fd50c959cfe0183fa167f GIT binary patch literal 351127 zcmZsCbyQo;yLFHj_X0(VyStZA+$m7pJwXeU0!2b`iWS#FaVYNIP#l6op-6Ep8Ymu; zkN3Uz{_gk3&01%jIVb02t;sX9XFq#C6Z2YIg#ect7XSbdsHrOI0RUJ$005>u_S45Z zaKa~@j~9es1%=lx3MvX7t{z_cZ*A-xR2Cou5!jt0K^y>4-(eE!{HsLKqF_=^K*(>57g(!zB0wc@;+NmkP$7f=465?jfhodYctPwhuuS3lzzMPFNf(QHVOniTf=C(%HZ-! z--sg4H`rcz7hTH-ZX6&+Ikbau;LHGv&aMbAT)1q%ok2hs?f$!pZ{pKE++}lFPQ+_r zem*~BMh8z65zT^Dxwt;Tdb#+!M09}lO)lQL57*3eby{GqXjr@vNNngl`lfA{!Vx{_ zgx3iywJ;@F;`r$m&q?goIf-GJY`;-OUWWXG6>saaLDrX%mbv)vh6Q{Omv`@tzGPu< zCQDM{QOB|J8#cq`hapDI?8U@ZE$<+Sn=8Xw%PFI1`Otjoo!2S!DaYor{p6YPBXU2& z{g226tjQEVGZ29T&8&;caH;n}1^XqSf}5Y~M{B*Dw@5(i8)xM3;r_IMd?||gZ0cL@ zDae4i3qRN&ROvH?SubBawkvku_thZM$v$(vX>ve4U!3^Yr>a6y}P4SUf!5EOkWy4m{Ly<8<+@KB*vwoF}m)iESCAmgvbDM`yD{ON@0IVl7BXzH&gW(2 zW#vilov-3N=YJ~Va#CAVkGl)b&|S%wH~qyc!(kusvbV3-UPEV}JTe@|hX2ER@k+9I zN{h~?Lv83+E-)SUh2Q{Tw;mMw0r z5EljhPS014!;bw<+UZil<_ypdpe{g2qSr$W`9QhsU*h1M|5O6OE`4pv@o~)!lyfpK3XI%yS3hcH--9LN?y1R_zqsHFwqdTe6@Fwv+1Paa!nkB2N8&41d;XdD2HfA6CM z08wB7=6^q<{doT8^ZD`k=bisLqkl#JpC_<*zGD1O8}kur0D#fk#QVn+&RbOzZvcRT z^`8SxO^@Xa0FVQyDZYB+hj#P@E5zU@95qSNqN>6Zy^mh_OYYS`WGFScUA$vO^{iM@L&d~26Gr#4v2H~@-)quM%0Vk5a z6W3#!+Nug^X=&BFWBE|1;;|JKS1WFs&6nKVDK;%VB4zzUM=xbm=pLiU(Ch83W>&BfuA82wl3T*h%bVhLArA&u>@xC+zJ%}W>|`eh zG0r3nO;i(@XQU}PBoB=;=l|64PtR+*y}dQ1u<3tQ+}sh}`i_F47TA%opC@sB_XRcY z)QwU)ZG=K(kvBIk7ZsC0S(Ngc=9R^d);I{{<~jocxx4ligVBpc4eiiRxXJ8+yeQWK z&C6DG%#lo5!LsL8S*6XV4u9%9qE$+_Gh-0o1}>G#HG6%xIFu)$1XeD_1lnGRcaP*c z?c2voQ(7#={!DtWUVD=mDn4

%Ev;9=SgwdYbJ@G)ty&Pp$VJ9fQ|60qLAD#Zj47 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�dl7_%4Cj{@1s2(-3SLk1o&WZ5P@$EoblZ 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%0AfJ4q7CZ94@C)S_0N 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%0A;xs-4qiGBoPXu9HJA9qDuDu`G1&#v8FJQ z;2Xuiv~8f#Q*K3Jl0+cGtVDecUdAkpy6+-}Pqk%M1Fw&8OxNZR{~Z2tiYnvYc_ z_4%`5rk|U-atz6ui`iyS+?^VVA|;93RYlv)Hk~G~QnnFk1<*YyPtYVBaC5AKVUi38 z=JDX+i@~jI=8RQgGX5hBkXe!Dr4M@dCY~}zzHE`!H`qzS7}uK?+jS@HO`LywT6LzE zwnBz<-RW$LJ4k#Lka#;fI;xIFuII8Ta{nrKkf)|n$zS$afjEG3+x)^FT!iR6uDTem zUp&lb{+zw^3K;IA~$jFf8Sjv@$c&}+Wk5HLkE4dodGCYwwmKU z1OHr{hazKW-2(A0?6Uu*XxR5p38XepoC}x?VBzy!1tB2JP;G9zvjJE$1}g^ByMx!3 zbi8%GSBYz@9d%pk9@&(^1%8cP

    1PfT&{0J@^1>(;sz3E#U3fWppH`lA2z+s3p` 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 literal 0 HcmV?d00001 diff --git a/assets/images/extensions-marketplace/ncs-worflow.svg b/assets/images/extensions-marketplace/ncs-worflow.svg new file mode 100644 index 000000000..949ba5855 --- /dev/null +++ b/assets/images/extensions-marketplace/ncs-worflow.svg @@ -0,0 +1 @@ +Implemented by youProvided by AgoraProvided by ActiveFenceUserUserWeb serverWeb serverAppAppSD-RTNSD-RTNActiveFenceActiveFenceLoginLogin authenticationJoin channelJoin channel withActiveFence activatedMonitor contentin channelContent matchesworkflow riskAction webhookAct on webhook. For example, useChannel Management REST APIto remove user from channelLog user outLogout \ No newline at end of file diff --git a/assets/images/flexible-classroom/ios-demo-qr.png b/assets/images/flexible-classroom/ios-demo-qr.png new file mode 100644 index 0000000000000000000000000000000000000000..e195ba853300981592f8e7fec98cdec0efbc579b GIT binary patch literal 2958 zcmV;93vu*`P)Br3kwT>e}Bft#{d8S^YimLIXTnQ)A#rHHa0dvK|y0M000XONkl8pYhpV;s|NrZ4%GRVwNU*Ng?%>1j#)SGR zL6Y|f&Ec1nrUI!zDv%1K0{OQ<;?Z6r>>|=C<_}S6xOTk#R=$D6(>Pemc!9UGG(=UY zT$VqaqHq)T^hB7ie~ffY#{9j6?9l4+jhXvlMl7;78<4O2k*W@P6RbdfB@p_aXyF{Vevy0MjMJ+poUNLW@m#lQdEa&Sb_YHKo}=h^ID?tvfpkaG`L={)>?!7O!%I= zs=Y3Tn(-_wzt*dm?h~zWl$=@4wr0BH)I{Ue8#9f(JE4jj*kF?O*E z`nA|*MPo-3$CRHXu=eA|)x`nB9yqZroJD&|&i0C22F{|8G)B{J^0!YyUwGc01p)IQ zg>XXwIjaenm?c4=128BnduUvF0karX-vDGd+SfK4e667Zd3qqw)g5Dg?YEDOG!hV- zjaXdWuDku^%VUL1EOJi|Chu43K3#=W)QrCQ1lVRInUO`IEU~zRlkS0f}R%SLt1ky06m0^$a;@ZQj(o%N@Rg81p z8C>1X(j?bd7J7`kqv<1@rnPFUoaSaAwxGHO#8)8OfHVpaXR6w7Xd$8S0ctGHQQ}yo z?F4II9$B^{uI_5nz9kMZniw+5XV%rlLg51l@|~p}y=bn5+L+{+wwLQ%o2rnjyUyF` zD(DcWs$Nq*n}=3%D`P=y?!bnQQf|uTmrxD3?w}E==w0SZY z4l0sxmAFisfn*w1AkPbA%tvB0AdHO!$wss#!Cf{2g~DH6CX~2|gesKYVGeXPcM z9Yl{6xXq|^k{Fu$>~P2Jc_z|Ckx)fqNW|(;nPY@%#Yx^&LrG=C5D$e>6y{wd$7^lE z(~=F9XsTB8ev|@n1wj0LK*|!BR3MKUTFre!i!t_=_@Z35qNbTjljV*1nz3^Yc_7fQ zMUh72i=ydQ(KMr-A?=V)m7q+&Y{Q()N!B4BOmtbuZ}QQ2EnAR=FjktPe*=)t#7CAW z`Yn5`Dv%cd5=}s&Nv*67byLP8CbX0Xf)Y*z%GU-;(IQQAPh4F_nvAMCs*i@wHT03w zO-N~}iWsZLM7?+5{VWHKOXy%{NEBAYA=*OST|Tt5fqb+mA1aVn0J4XTG|hcLI3GC= zgpno>0;`KWRutq%D>9&Z<0uML5*@rU4M8F@pn?wNYmr#p_la(%Z6RMv7qJXE19gZD zX@rZ1YDIxs!kWgsUd!b*R<<2Rwx}-JyjBtp-YPu%F(9r2c}gIz97w}hK^91)JLl!_ z6tHY;>~PCHGIo*GMY9Alb}2WWBw$9F{4R7dWCDgWm>=Qvo1&Kek$TclE`wNIq*g{r z2hY;JmV6Lt= zDd@6kscxPzN&m!26A@oa)ymAqJVr7ZY%*u)f*%TFzcQoeOloCUkbzB;IV-}7tw4N% z$kzs>EO$r+!biw#YUK?;oEDAS1|;Nn?9V;ft>BqWzD%Y%G)s8FzFWT=kSU)-Avv)V z^;l2DAx0A`nzvEIUYd~yEIu+!zgvsE8uxjnp-hMkyFHQ3Ky3DJqbMto3Pe&XH^gNs z5?07++#S+V!3@kyrYf0WR(%11Cql}HAPEY(?MYd+ zq|DOxJ*KMd3Be>Ef+T0Eu?ykLV?VYHl{Pt;Y5&L%1F?niH6X46*#^YredLD#DM`0Z zcg|}0?6@G7yQOe%rWb)UmeNvfNPy4noK-RmQYZFn`|c=B_%dl6b5Eq?NCHj3q-j6B zGz=RXY5af#xeg|K+dL&a?qsnFpHl?*_(Se*?tQD#i#ZyY)A9g&D%a%8?B>Jr>9}Wh8qyl+T zAWc3a)GGifI7HjGN^W)0*pEUg0Os^1=0x_wm zC07^ekb{(iDhL!VbFMD4R~IExW<2RsHJfD4yE9hnS4qEqC$8C*C*rile)t&EJjc{TS*+!Tc`B=OanC6X_l6!J1qf4HANJaE(#fI`Bx{R<|aCHxP zI+tU8L^_D;kb`oZKq`<5Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Agora Video SDK engine:agoraEngine = RtcEngine.createEnable the video module:agoraEngine.enableVideo()Set the channel profile:agoraEngine.setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING)HostStart a live streaming eventRetrieve authentication token to join channelSet the role as host:agoraEngine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER)Setup local video:agoraEngine.setupLocalVideo(VideoCanvas)Start local preview:agoraEngine.startPreview()Join a channel as host:agoraEngine.joinChannelSend data streamAudienceJoin a live streaming eventRetrieve authentication token to join channelSet the user role as audience:agoraEngine.setClientRole(CLIENT_ROLE_AUDIENCE)Join the channel:agoraEngine.joinChannelRetrieve streaming from the hosts:agoraEngine.setupRemoteVideoReceive data streamLeave the live streaming eventLeave the channel:agoraEngine.leaveChannel()Close appClean up local resourcesagoraEngine.destroy() \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Agora Video SDK engine:agoraEngine = RtcEngine.createEnable the video module:agoraEngine.enableVideo()Set the channel profile:agoraEngine.setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING)HostStart a live streaming eventRetrieve authentication token to join channelSet the role as host:agoraEngine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER)Setup local video:agoraEngine.setupLocalVideo(VideoCanvas)Start local preview:agoraEngine.startPreview()Join a channel as host:agoraEngine.joinChannelSend data streamAudienceJoin a live streaming eventRetrieve authentication token to join channelSet the user role as audience:agoraEngine.setClientRole(CLIENT_ROLE_AUDIENCE)Join the channel:agoraEngine.joinChannelRetrieve streaming from the hosts:agoraEngine.setupRemoteVideoReceive data streamLeave the live streaming eventLeave the channel:agoraEngine.leaveChannel()Close appClean up local resourcesagoraEngine.destroy() \ No newline at end of file diff --git a/assets/images/interactive-live-streaming/ils-call-logic-flutter.svg b/assets/images/interactive-live-streaming/ils-call-logic-flutter.svg index c3f5006ad..38b2888b0 100644 --- a/assets/images/interactive-live-streaming/ils-call-logic-flutter.svg +++ b/assets/images/interactive-live-streaming/ils-call-logic-flutter.svg @@ -1,488 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Agora Video SDK engineagoraEngine = RtcEngine.createEnable the video module:agoraEngine.enableVideoHostStart a live streaming eventRetrieve authentication token to join channelEnable live streaming in the channel:agoraEngine.setChannelProfile(ChannelProfile.LiveBroadcasting)Set the role as host:agoraEngine.setClientRole(ClientRole.Broadcaster)Join a channel as host:agoraEngine.joinChannelon "joinChannelSuccess"PublishSend data streamWidget = RtcLocalView.SurfaceView()AudienceJoin a live streaming eventRetrieve authentication token to join channelSet the user role as audience:agoraEngine.setClientRole(ClientRole.Audience)Join the channel:agoraEngine.joinChannelon "joinChannelSuccess"Retrieve streaming from the hosts:on "userJoined"Receive data streamWidget = RtcRemoteView.SurfaceView()Leave broadcastagoraEngine.leaveChannel()Close appClean up local resourcesagoraEngine.destroy() \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Agora Video SDK engineagoraEngine = RtcEngine.createEnable the video module:agoraEngine.enableVideoHostStart a live streaming eventRetrieve authentication token to join channelEnable live streaming in the channel:agoraEngine.setChannelProfile(ChannelProfile.LiveBroadcasting)Set the role as host:agoraEngine.setClientRole(ClientRole.Broadcaster)Join a channel as host:agoraEngine.joinChannelon "joinChannelSuccess"PublishSend data streamWidget = RtcLocalView.SurfaceView()AudienceJoin a live streaming eventRetrieve authentication token to join channelSet the user role as audience:agoraEngine.setClientRole(ClientRole.Audience)Join the channel:agoraEngine.joinChannelon "joinChannelSuccess"Retrieve streaming from the hosts:on "userJoined"Receive data streamWidget = RtcRemoteView.SurfaceView()Leave broadcastagoraEngine.leaveChannel()Close appClean up local resourcesagoraEngine.destroy() \ No newline at end of file diff --git a/assets/images/interactive-live-streaming/ils-call-logic-ios.svg b/assets/images/interactive-live-streaming/ils-call-logic-ios.svg index b925b4213..1c32a1c60 100644 --- a/assets/images/interactive-live-streaming/ils-call-logic-ios.svg +++ b/assets/images/interactive-live-streaming/ils-call-logic-ios.svg @@ -1,484 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitiate the Video SDK engine:agoraEngine = AgoraRtcEngineKit.sharedEngineStart video in the engine:agoraEngine.enableVideo()HostStart a live streaming eventIn an live streaming event, only the hosts broadcast to the channel:agoraEngine.setClientRole(.broadcaster)Start local video:agoraEngine.setupLocalVideo(videoCanvas)Join the channel:agoraEngine?.joinChannelSend data streamAudienceJoin live streaming eventIn an live streaming event, the audience views the stream sent by channel hosts:agoraEngine.setClientRole(.audience)Join the channel:agoraEngine?.joinChannelRetrieve streaming from the hosts:agoraEngine.setupRemoteVideo(videoCanvas)Receive data streamsLeave live streaming eventStop local video:agoraEngine.stopPreview()Leave the channel:agoraEngine.leaveChannel(nil)Close appClean up local resources:AgoraRtcEngineKit.destroy() \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitiate the Video SDK engine:agoraEngine = AgoraRtcEngineKit.sharedEngineStart video in the engine:agoraEngine.enableVideo()HostStart a live streaming eventIn an live streaming event, only the hosts broadcast to the channel:agoraEngine.setClientRole(.broadcaster)Start local video:agoraEngine.setupLocalVideo(videoCanvas)Join the channel:agoraEngine?.joinChannelSend data streamAudienceJoin live streaming eventIn an live streaming event, the audience views the stream sent by channel hosts:agoraEngine.setClientRole(.audience)Join the channel:agoraEngine?.joinChannelRetrieve streaming from the hosts:agoraEngine.setupRemoteVideo(videoCanvas)Receive data streamsLeave live streaming eventStop local video:agoraEngine.stopPreview()Leave the channel:agoraEngine.leaveChannel(nil)Close appClean up local resources:AgoraRtcEngineKit.destroy() \ No newline at end of file diff --git a/assets/images/interactive-live-streaming/ils-call-logic-template.svg b/assets/images/interactive-live-streaming/ils-call-logic-template.svg index da5be5cdf..947f772d7 100644 --- a/assets/images/interactive-live-streaming/ils-call-logic-template.svg +++ b/assets/images/interactive-live-streaming/ils-call-logic-template.svg @@ -1,484 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitiate the Video SDK engine.Start video in the engine.HostStart ILS eventIn an ILS event, only the hosts broadcast to the channel.Start local video.Join the channel.Send data stream.AudienceJoin ILS eventIn an ILS event, the audience views the broadcast made by channel hosts.Join the channel.Retrieve streaming from the other user.Receive data streamsLeave ILS eventStop local video.Leave the channel.Close appClean up local resources. \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitiate the Video SDK engine.Start video in the engine.HostStart ILS eventIn an ILS event, only the hosts broadcast to the channel.Start local video.Join the channel.Send data stream.AudienceJoin ILS eventIn an ILS event, the audience views the broadcast made by channel hosts.Join the channel.Retrieve streaming from the other user.Receive data streamsLeave ILS eventStop local video.Leave the channel.Close appClean up local resources. \ No newline at end of file diff --git a/assets/images/interactive-live-streaming/ils-call-logic-unity.svg b/assets/images/interactive-live-streaming/ils-call-logic-unity.svg index 70d108cd5..20af4dabe 100644 --- a/assets/images/interactive-live-streaming/ils-call-logic-unity.svg +++ b/assets/images/interactive-live-streaming/ils-call-logic-unity.svg @@ -1,494 +1 @@ -Your gameAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen gameInitiate the Agora Video SDK engine:agoraEngine=IRtcEngine.GetEngine()Setup the local video stream:agoraEngine.EnableVideo()agoraEngine.EnableVideoObserver()HostStart a live streaming eventRetrieve authentication token to join channelEnable live streaming in the channel:agoraEngine.SetChannelProfile(CHANNEL_PROFILE.CHANNEL_PROFILE_LIVE_BROADCASTING)Set the user role as host:agoraEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER)Join a channel as host:agoraEngine.JoinChannelByKey()Send data streamAudienceJoin the live streaming eventRetrieve authentication token to join channelSet the user role as audience:agoraEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_AUDIENCE)Join the channel:agoraEngine.JoinChannelByKey()A callback to start remote video:onUserJoined()Retrieve streaming from the hosts:RemoteView.SetForUser(uid)Receive data StreamLeave the live streaming eventLeave the channel:agoraEngine.leaveChannel()Stop local video stream:agoraEngine.DisableVideo()Disable the video observer:agoraEngine.DisableVideoObserver()Close gameClean up local resources:agoraEngine.destroy() \ No newline at end of file +Your gameAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen gameInitiate the Agora Video SDK engine:agoraEngine=IRtcEngine.GetEngine()Setup the local video stream:agoraEngine.EnableVideo()agoraEngine.EnableVideoObserver()HostStart a live streaming eventRetrieve authentication token to join channelEnable live streaming in the channel:agoraEngine.SetChannelProfile(CHANNEL_PROFILE.CHANNEL_PROFILE_LIVE_BROADCASTING)Set the user role as host:agoraEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER)Join a channel as host:agoraEngine.JoinChannelByKey()Send data streamAudienceJoin the live streaming eventRetrieve authentication token to join channelSet the user role as audience:agoraEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_AUDIENCE)Join the channel:agoraEngine.JoinChannelByKey()A callback to start remote video:onUserJoined()Retrieve streaming from the hosts: RemoteView.SetForUser(uid)Receive data StreamLeave the live streaming eventLeave the channel:agoraEngine.leaveChannel()Stop local video stream:agoraEngine.DisableVideo()Disable the video observer:agoraEngine.DisableVideoObserver()Close gameClean up local resources:agoraEngine.destroy() \ No newline at end of file diff --git a/assets/images/interactive-live-streaming/ils-call-logic-web.svg b/assets/images/interactive-live-streaming/ils-call-logic-web.svg index 3bbb766c5..46e2f8dc1 100644 --- a/assets/images/interactive-live-streaming/ils-call-logic-web.svg +++ b/assets/images/interactive-live-streaming/ils-call-logic-web.svg @@ -1,484 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Video SDK engine:agoraEngine = AgoraRTC.createClientSet the required event listners:agoraEngine.on("user-published")agoraEngine.on("user-unpublished")HostStart live streaming eventRetrieve authentication token to join channelSet the user role as host:agoraEngine.setClientRole("host")Join a channel as host:agoraEngine.joinCreate local media tracks :AgoraRTC.createMicrophoneAudioTrackAgoraRTC.createCameraVideoTrackPush local media tracks to the channel:agoraEngine.publishStop the remote video and play the local video:rtc.localVideoTrack.playrtc.remoteVideoTrack.stopRetrieve streaming from the other user:agoraEngine.on("user-published")AudienceJoin live streaming eventRetrieve authentication token to join channelSet the user role as audience:agoraEngine.setClientRole("audience")Join the live streaming event:agoraEngine.joinRetrieve streaming from the other user:agoraEngine.on("user-published")agoraEngine.subscribeStop the local video and play the remote video:rtc.localVideoTrack.stoprtc.remoteVideoTrack.playReceive data streamLeave live streaming eventagoraEngine.leave \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Video SDK engine:agoraEngine = AgoraRTC.createClientStart video in the engine:App.initStart local media:stream = AgoraRTC.createStreamstream.initstream.playHostStart live streaming eventRetrieve authentication token to join channelSet the user role as host:agoraEngine.setClientRole("host")Join a channel as host:agoraEngine.joinPush local media to the channel:agoraEngine.publishAudienceJoin live streaming eventRetrieve authentication token to join channelSet the user role as audience:agoraEngine.setClientRole("audience")Join the live streaming event:agoraEngine.joinRetrieve streaming from the other user:agoraEngine.on("stream-added")agoraEngine.subscribeagoraEngine.on("stream-subscribed")Receive data streamLeave live streaming eventagoraEngine.leave \ No newline at end of file diff --git a/assets/images/interactive-live-streaming/live-streaming-over-multiple-channels.svg b/assets/images/interactive-live-streaming/live-streaming-over-multiple-channels.svg new file mode 100644 index 000000000..90d3f2dde --- /dev/null +++ b/assets/images/interactive-live-streaming/live-streaming-over-multiple-channels.svg @@ -0,0 +1 @@ +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Agora engineJoin a channelStart live streamingSet the user role as hostJoin a channel as hostPublish local media to the channelChannel media relayStart multi-channel live streamingSet the source channel info:Source channel name, token, and uidSet the destination channel info:Destination channel name, token, and uidCall the method tostart media relayingRelay stream to thedestination channelReport the media relayingstate with a callback functionsJoin multiple channelsStart multi-channel live streamingCreate a new channelSet the user role as hostfor the new channelJoin the new channelJoin acceptedPublish to the new channelLeave the live streaming eventStop media relayingLeave all the channels \ No newline at end of file diff --git a/assets/images/iot/iot-channel-quality.svg b/assets/images/iot/iot-channel-quality.svg index 9e5b5be9a..84e5ce9bd 100644 --- a/assets/images/iot/iot-channel-quality.svg +++ b/assets/images/iot/iot-channel-quality.svg @@ -1 +1 @@ -Your appAgoraUserUserIoT SDKIoT SDKSD-RTNSD-RTNSetup Agora engineInstantiate the Agora RTC engineSet options includinglog file path and logging levelVerify license and initialize the engineCreate a connectionJoin a channelJoin a channelSet bandwidth estimation parametersSpecify the audio codec, sampling rate,and the number of channelsCall the join channel methodon join channel success callbackSend and receive audio and videoStart threads to send audio and video dataReceive audio and video dataRender audio and video framesDetect and respond to network bandwidth changesOn target bitrate changed callbackAdjust the sending bit rate and resolutionOn key frame generation request callbackSend a key frameManage audio and video streamsMute local audio or videoCall mute local audio ormute local videoMute remote audio or videoCall mute remote audio ormute remote videoOn user mute audio callbackSuspend or resume audio feedOn user mute video callbackSuspend or resume video feed \ No newline at end of file +Your appAgoraUserUserIoT SDKIoT SDKSD-RTNSD-RTNSetup Agora engineInstantiate the Agora RTC engineSet options includinglog file path and logging levelVerify license and initialize the engineCreate a connectionJoin a channelJoin a channelSet bandwidth estimation parametersSpecify the audio codec, sampling rate,and the number of channelsCall the join channel methodon join channel success callbackSend and receive audio and videoStart threads to send audio and video dataReceive audio and video dataRender audio and video framesDetect and respond to network bandwidth changesOn target bitrate changed callbackAdjust the sending bit rate and resolutionOn key frame generation request callbackSend a key frameManage audio and video streamsMute local audio or videoCall mute local audio ormute local videoMute remote audio or videoCall mute remote audio ormute remote videoOn user mute audio callbackSuspend or resume audio feedOn user mute video callbackSuspend or resume video feed \ No newline at end of file diff --git a/assets/images/iot/iot-get-started.svg b/assets/images/iot/iot-get-started.svg index bfdcf514e..6d11e9764 100644 --- a/assets/images/iot/iot-get-started.svg +++ b/assets/images/iot/iot-get-started.svg @@ -1 +1 @@ -Your appAgoraUserUserIoT SDKIoT SDKSD-RTNSD-RTNSet up Agora engineInstantiate the Agora engineSet engine optionsVerify license and initialize the engineCreate a connectionJoin a channelJoin a channelCall the method to join a channelOn join channel success callbackSend audio and videoStart thread to send audio dataStart thread to send video dataReceive audio and videoOn audio data callbackRender audio frameOn video data callbackRender video frameLeave channelLeave channelCall the leave channel methodClean upDestroy the connectionCall the finish method to release resources +Your appAgoraUserUserIoT SDKIoT SDKSD-RTNSD-RTNSet up Agora engineInstantiate the Agora engineSet engine optionsVerify license and initialize the engineCreate a connectionJoin a channelJoin a channelCall the method to join a channelOn join channel success callbackSend audio and videoStart thread to send audio dataStart thread to send video dataReceive audio and videoOn audio data callbackRender audio frameOn video data callbackRender video frameLeave channelLeave channelCall the leave channel methodClean upDestroy the connectionCall the finish method to release resources \ No newline at end of file diff --git a/assets/images/iot/iot-licensing.svg b/assets/images/iot/iot-licensing.svg index ad925f79f..d958d4972 100644 --- a/assets/images/iot/iot-licensing.svg +++ b/assets/images/iot/iot-licensing.svg @@ -1 +1 @@ -Your IoT deviceAgoraYouYouIoT SDKIoT SDKAgora SalesAgora SalesREST APIREST APIContact Agora sales to request a licenseSend license informationActivate the licenseSend activation infoWrite a license to the device \ No newline at end of file +Your IoT deviceAgoraYouYouIoT SDKIoT SDKAgora SalesAgora SalesREST APIREST APIContact Agora sales to request a licenseSend license informationActivate the licenseSend activation infoWrite a license to the device \ No newline at end of file diff --git a/assets/images/iot/iot-multi-channel.svg b/assets/images/iot/iot-multi-channel.svg index 9b6dec9c1..814d8610e 100644 --- a/assets/images/iot/iot-multi-channel.svg +++ b/assets/images/iot/iot-multi-channel.svg @@ -1 +1 @@ -Your appAgoraUserUserIoT SDKIoT SDKSD-RTNSD-RTNSetup Agora engineInstantiate the Agora engineSet engine optionsVerify license and initializeCreate multiple connectionsCreate connection-1Create connection-2Stream to multiple channelsJoin channelsJoin channel-1 using connectionId-1Join channel-2 using connectionId-2Send audio and video data usingconnectionId-1 to stream to channel-1Send audio and video data usingconnectionId-2 to stream to channel-2Push multiple streams to a single channelJoin channelJoin channel-1 usingconnectionId-1 and userId-1Join channel-1 usingconnectionId-2 and userId-2Send audio and video datausing connectionId-1Send audio and video datausing connectionId-2Leave channel(s)Leave channelCall leave channel using connectionId-1Call leave channel using connectionId-2Clean upCall destroy connectionusing connectionId-1Call destroy connectionusing connectionId-2Call the finish method to release resources \ No newline at end of file +Your appAgoraUserUserIoT SDKIoT SDKSD-RTNSD-RTNSetup Agora engineInstantiate the Agora engineSet engine optionsVerify license and initializeCreate multiple connectionsCreate connection-1Create connection-2Stream to multiple channelsJoin channelsJoin channel-1 using connectionId-1Join channel-2 using connectionId-2Send audio and video data usingconnectionId-1 to stream to channel-1Send audio and video data usingconnectionId-2 to stream to channel-2Push multiple streams to a single channelJoin channelJoin channel-1 usingconnectionId-1 and userId-1Join channel-1 usingconnectionId-2 and userId-2Send audio and video datausing connectionId-1Send audio and video datausing connectionId-2Leave channel(s)Leave channelCall leave channel using connectionId-1Call leave channel using connectionId-2Clean upCall destroy connectionusing connectionId-1Call destroy connectionusing connectionId-2Call the finish method to release resources \ No newline at end of file diff --git a/assets/images/media-gateway/media-gateway-flow.svg b/assets/images/media-gateway/media-gateway-flow.svg deleted file mode 100644 index 21e8512b9..000000000 --- a/assets/images/media-gateway/media-gateway-flow.svg +++ /dev/null @@ -1 +0,0 @@ -Signal SourceTranscodingMediaGatewaySD-RTNAudienceEnhance imagequalityRTMPSRTRTC \ No newline at end of file diff --git a/assets/images/media-gateway/obs-server-setting.png b/assets/images/media-gateway/obs-server-setting.png deleted file mode 100644 index cfc78713876344ea854eece3886207e73f35926b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17561 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

    j+=|1PQKPzSIhY z8GJ~HX9-Ot|JwMIetAKpvp7(8|8fesh?QEUf1-_}%?n7h9VcSWDxoLuBdML3b*DF6 zb9R=cK>Gr7L#*m}t=K9REorE_MRV{w-^&#tK#y+-Ld z6UG2EHZ4e+tw)b;xxza1blI>u_&W-djDyCA1nDmO1|$|BXWP`oOub7WCWZCyw^KYo zQ32-OxL;bYUu^u&wV~thD!z?)s|)Y7mlB^mExkVfE_~ymGlsPr-C9B`Y$+(#6$qWC z1SScSY~H^MzgnoOea&Dd`x3%(ctyU+KzrwxdX%WFq_Q6TzO$Ea5x>&^?@Bo<-u{-iLBFp`NQLm zA#!<)_|B_#PGq(O;!FP5hTF{PBghC_eP@Q-~WhYt5o{(Bd)Ur6@vU4NtR z6#v{U5Mr77d+Ez}Y>U4S(0oRy`}+Vo_5W7g8 literal 0 HcmV?d00001 diff --git a/assets/images/signaling/stream-channel-workflow.puml b/assets/images/signaling/stream-channel-workflow.puml index 2b8cc292c..b24108c38 100644 --- a/assets/images/signaling/stream-channel-workflow.puml +++ b/assets/images/signaling/stream-channel-workflow.puml @@ -62,5 +62,4 @@ USR -> APP: Log out APP -> API: Log out of Signaling end -@enduml - +@enduml \ No newline at end of file diff --git a/assets/images/signaling/stream-channel-workflow.svg b/assets/images/signaling/stream-channel-workflow.svg index ab59cbae5..f157a924b 100644 --- a/assets/images/signaling/stream-channel-workflow.svg +++ b/assets/images/signaling/stream-channel-workflow.svg @@ -1 +1 @@ -Your appAgoraUserUserSignaling SDKSignaling SDKSignalingSignalingInitializeOpen appCreate a signalingEngine instanceAdd user event callbacksSet the authentication tokenLog in to SignalingChannelCreate stream channelCreate channel or add user to channelSubscribe to a channelChannel eventsTopicJoin topicCreate topic or add user to topicSubscribe to topicTopic eventsMessagesSend messagePublish message to topicMessage from topicListen for message eventsReceive messageLeave topicLeave topicLeave channelLeave channelPresenceChange statuslisten for user eventsConnectionStateChangedInform usersCloseLog outLog out of Signaling \ No newline at end of file +Your appAgoraUserUserSignaling SDKSignaling SDKSignalingSignalingInitializeOpen appCreate a signalingEngine instanceAdd user event callbacksSet the authentication tokenLog in to SignalingChannelCreate stream channelCreate channel or add user to channelSubscribe to a channelChannel eventsTopicJoin topicCreate topic or add user to topicSubscribe to topicTopic eventsMessagesSend messagePublish message to topicMessage from topicListen for message eventsReceive messageLeave topicLeave topicLeave channelLeave channelPresenceChange statuslisten for user eventsConnectionStateChangedInform usersCloseLog outLog out of Signaling \ No newline at end of file diff --git a/assets/images/video-calling/geofencing.svg b/assets/images/video-calling/geofencing.svg index bf480faab..1ded373b7 100644 --- a/assets/images/video-calling/geofencing.svg +++ b/assets/images/video-calling/geofencing.svg @@ -1,444 +1 @@ -Implemented by youProvided by AgoraUserUserAppAppSD-RTNSD-RTNStart the appGeofencingSet SD-RTN region in the Agoraengine configurationInitiate the Agora engineConnect to SD-RTN in aspecific regionSuccess responseSelect a channel to joinJoin a channel with userId, channel name, and tokenJoin accepted \ No newline at end of file +Implemented by youProvided by AgoraUserUserAppAppSD-RTNSD-RTNStart the appGeofencingSet SD-RTN region in the Agoraengine configurationInitiate the Agora engineConnect to SD-RTN in aspecific regionSuccess responseSelect a channel to joinJoin a channel with userId, channel name, and tokenJoin accepted \ No newline at end of file diff --git a/assets/images/video-calling/process-raw-video-audio.svg b/assets/images/video-calling/process-raw-video-audio.svg index b01432ed0..7c93f7595 100644 --- a/assets/images/video-calling/process-raw-video-audio.svg +++ b/assets/images/video-calling/process-raw-video-audio.svg @@ -1,478 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppCreate an instance of Agora Engine using Video SDKEnable audio and video in Agora EngineSetup raw data processingSetup the audio frame observerSetup the video frame observerJoinJoin a channelRegister the video frame observerRegister the audio frame observerSet audio frame parametersRetrieve authentication token to join a channelJoin the channelProcess raw audio and video dataGet the raw data in the callbacksSend the processed data back with the callbacksLeaveLeave the channelUnegister the video frame observerUnegister the audio frame observerLeave the channel \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppCreate an instance of Agora Engine using Video SDKEnable audio and video in Agora EngineSetup raw data processingSetup the audio frame observerSetup the video frame observerJoinJoin a channelRegister the video frame observerRegister the audio frame observerSet audio frame parametersRetrieve authentication token to join a channelJoin the channelProcess raw audio and video dataGet the raw data in the callbacksSend the processed data back with the callbacksLeaveLeave the channelUnegister the video frame observerUnegister the audio frame observerLeave the channel \ No newline at end of file diff --git a/assets/images/video-calling/video-call-logic-android.svg b/assets/images/video-calling/video-call-logic-android.svg index 22e0fb1be..cd0dd558f 100644 --- a/assets/images/video-calling/video-call-logic-android.svg +++ b/assets/images/video-calling/video-call-logic-android.svg @@ -1,470 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Agora Video SDK engine:agoraEngine = RtcEngine.createSetup the local video stream:agoraEngine.enableVideo()agoraEngine.setupLocalVideo(VideoCanvas)UserJoin a callRetrieve authentication token to join channelJoin the channel:agoraEngine.joinChannel()Retrieve streaming from the other user:agoraEngine.setupRemoteVideo(VideoCanvas)Receive and send data streamLeave the callLeave the channel:agoraEngine.leaveChannel()Close appClean up local resources:agoraEngine.destroy() \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Agora Video SDK engine:agoraEngine = RtcEngine.createSetup the local video stream:agoraEngine.enableVideo()agoraEngine.setupLocalVideo(VideoCanvas)UserJoin a callRetrieve authentication token to join channelJoin the channel:agoraEngine.joinChannel()Retrieve streaming from the other user:agoraEngine.setupRemoteVideo(VideoCanvas)Receive and send data streamLeave the callLeave the channel:agoraEngine.leaveChannel()Close appClean up local resources:agoraEngine.destroy() \ No newline at end of file diff --git a/assets/images/video-calling/video-call-logic-flutter.svg b/assets/images/video-calling/video-call-logic-flutter.svg index 2767cfd95..bf3eae44b 100644 --- a/assets/images/video-calling/video-call-logic-flutter.svg +++ b/assets/images/video-calling/video-call-logic-flutter.svg @@ -1,464 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Agora Video SDK engine:agoraEngine = RtcEngine.createSetup the local video stream:agoraEngine.enableVideoWidget = RtcLocalView.SurfaceView()UserJoin a callRetrieve authentication token to join channelJoin the channel:agoraEngine.joinChannelRetrieve streaming from the other user:Widget = RtcRemoteView.SurfaceView()Receive and send data streamLeave callLeave the channelagoraEngine.leaveChannelClose appClean up local resources:agoraEngine.destroy() \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Agora Video SDK engine:agoraEngine = RtcEngine.createSetup the local video stream:agoraEngine.enableVideoWidget = RtcLocalView.SurfaceView()UserJoin a callRetrieve authentication token to join channelJoin the channel:agoraEngine.joinChannelRetrieve streaming from the other user:Widget = RtcRemoteView.SurfaceView()Receive and send data streamLeave callLeave the channelagoraEngine.leaveChannelClose appClean up local resources:agoraEngine.destroy() \ No newline at end of file diff --git a/assets/images/video-calling/video-call-logic-ios.svg b/assets/images/video-calling/video-call-logic-ios.svg index 24a90881b..939684c80 100644 --- a/assets/images/video-calling/video-call-logic-ios.svg +++ b/assets/images/video-calling/video-call-logic-ios.svg @@ -1,468 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitiate the Video SDK engine:agoraEngine = AgoraRtcEngineKit.sharedEngineSetup the local video stream:agoraEngine.enableVideo()HostStart a callIn a call, all users send to the channel:agoraEngine.setClientRole(.broadcaster)Start local video:agoraEngine.setupLocalVideo(videoCanvas)Join the channel:agoraEngine?.joinChannelRetrieve streaming from the other user:agoraEngine.setupRemoteVideo(videoCanvas)Receive and send data streamsLeave the callStop local video:agoraEngine.stopPreview()Leave the channel:agoraEngine.leaveChannel(nil)Close appClean up local resources:AgoraRtcEngineKit.destroy() \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitiate the Video SDK engine:agoraEngine = AgoraRtcEngineKit.sharedEngineSetup the local video stream:agoraEngine.enableVideo()HostStart a callIn a call, all users send to the channel:agoraEngine.setClientRole(.broadcaster)Start local video:agoraEngine.setupLocalVideo(videoCanvas)Join the channel:agoraEngine?.joinChannelRetrieve streaming from the other user:agoraEngine.setupRemoteVideo(videoCanvas)Receive and send data streamsLeave the callStop local video:agoraEngine.stopPreview()Leave the channel:agoraEngine.leaveChannel(nil)Close appClean up local resources:AgoraRtcEngineKit.destroy() \ No newline at end of file diff --git a/assets/images/video-calling/video-call-logic-template.svg b/assets/images/video-calling/video-call-logic-template.svg index 571701706..163ea25d6 100644 --- a/assets/images/video-calling/video-call-logic-template.svg +++ b/assets/images/video-calling/video-call-logic-template.svg @@ -1,468 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitiate the Video SDK engine.Start video in the engine.HostStart callIn a call, all users broadcast to the channel.Start local video.Join the channel.Retrieve streaming from the other user.Receive and send data streamsLeave callStop local video.Leave the channel.Close appClean up local resources. \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitiate the Video SDK engine.Start video in the engine.HostStart callIn a call, all users broadcast to the channel.Start local video.Join the channel.Retrieve streaming from the other user.Receive and send data streamsLeave callStop local video.Leave the channel.Close appClean up local resources. \ No newline at end of file diff --git a/assets/images/video-calling/video-call-logic-unity.svg b/assets/images/video-calling/video-call-logic-unity.svg index ee8bb32ad..79330c265 100644 --- a/assets/images/video-calling/video-call-logic-unity.svg +++ b/assets/images/video-calling/video-call-logic-unity.svg @@ -1,472 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen gameInitiate the Agora Video SDK engine:agoraEngine=IRtcEngine.GetEngine()Setup the local video stream:agoraEngine.EnableVideo()agoraEngine.EnableVideoObserver()UserStart callJoin the channel:agoraEngine.JoinChannelByKey()Join callA callback to start remote video:OnUserJoined()Retrieve streaming from the other user:RemoteView.SetForUser(uid)Receive and send data streamLeave callLeave the channel:agoraEngine.leaveChannel()Stop local video stream:agoraEngine.DisableVideo()Disable the video observer:agoraEngine.DisableVideoObserver()Close gameClean up local resources:agoraEngine.destroy() \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen gameInitiate the Agora Video SDK engine:agoraEngine=IRtcEngine.GetEngine()Setup the local video stream:agoraEngine.EnableVideo()agoraEngine.EnableVideoObserver()UserStart callJoin the channel:agoraEngine.JoinChannelByKey()Join callA callback to start remote video:OnUserJoined()Retrieve streaming from the other user:RemoteView.SetForUser(uid)Receive and send data streamLeave callLeave the channel:agoraEngine.leaveChannel()Stop local video stream:agoraEngine.DisableVideo()Disable the video observer:agoraEngine.DisableVideoObserver()Close gameClean up local resources:agoraEngine.destroy() \ No newline at end of file diff --git a/assets/images/video-calling/video-call-logic-web.svg b/assets/images/video-calling/video-call-logic-web.svg index 233a4544d..a8ac6ff5f 100644 --- a/assets/images/video-calling/video-call-logic-web.svg +++ b/assets/images/video-calling/video-call-logic-web.svg @@ -1,464 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Video SDK engine:agoraEngine = AgoraRTC.createClientSet the required event listners:agoraEngine.on("user-published")agoraEngine.on("user-unpublished")UserStart callRetrieve authentication token to join channelJoin a channel:agoraEngine.joinJoin acceptedCreate local media tracks :AgoraRTC.createMicrophoneAudioTrackAgoraRTC.createCameraVideoTrackPush local media tracks to the channel:agoraEngine.publishRetrieve streaming from the other user:agoraEngine.on("user-published")Play remote media tracks: remoteVideoTrack.playremoteAudioTrack.playReceive and send data streamsLeave callleave the channel:agoraEngine.leave \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Video SDK engine:agoraEngine = AgoraRTC.createClientSet the required event listners:agoraEngine.on("user-published")agoraEngine.on("user-unpublished")UserStart callRetrieve authentication token to join channelJoin a channel:agoraEngine.joinJoin acceptedCreate local media tracks :AgoraRTC.createMicrophoneAudioTrackAgoraRTC.createCameraVideoTrackPush local media tracks to the channel:agoraEngine.publishRetrieve streaming from the other user:agoraEngine.on("user-published")Play remote media tracks: remoteVideoTrack.playremoteAudioTrack.playReceive and send data streamsLeave callleave the channel:agoraEngine.leave \ No newline at end of file diff --git a/assets/images/video-calling/video-composite-example.png b/assets/images/video-calling/video-composite-example.png deleted file mode 100644 index 3430e448c6eabdbeb38fcda487e08c1f2510fef9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9449 zcmeHtcUV(f)31uk5f11%C@2VcEU0urLP#ib6cv#Up<@Bul+a1&pdz3maug7d1VyEV z0K$gQgNT5X1SugT^co-mq$HG3zTo@*cc1&*@BVS`_s^Rr``J5duQhAtH#2Kyt;sVJ zqZ>j3;sSg2>=DwxdChFk9zJl-o_zwp@$p)k{H85LuzlK2}bD8_n&^Tj;y%*W(x7N^Zl2fk3RKH|K#rM?5uOQn`j*W zk2B=q)i+fO1JfHZMXdSd&6@6&<<+gMhQ%?~24#G$xP93q^4m4fTs>s&HDq33`j5DR z5&ZWh>csl&;wF1-Ykp;OaC)P>V`Y)E)zZ)5Zf$>|u&3s^Ya81?M%I!E)5urfU+QlT z@_2;?m>HSxIcW*RhX6bd{=kYcP4dHt!-?J~Zqq3)u%Cv(H+spRIQ*q*1)Be~smJL__b z(vkauHzz-Wl#YoMoh^Yqf0)9!)M27<-OfgNU6khbVzQs_49h#Qs6o1JdrRW`IM=GR z5h}Rvi%sPS{UXc9i6j{IbVEveV7>LmM7iw2P6R&|2? z?5|z9gi39*X3#zS87PQxcUaH)m|c+}J+pYHq6uUe_)6NE4{&)5dIAhL6}xP=OxULf z%Ozhi$B6r?FSa5ULgrzq%#=1W{}NlT?#rF$r?Q<4)UDYwb6ui1ibx5{s&xDz4>H;h znmkE{o?G_1;3)DHO={A4YF-$gDv@FU45r;T^GA<|WCz0Re?BR6y|Hh1zDHnpYuV}H zg%^o!;lzvHT~Uz9R?%EtHGt^u&iVfI#_#N2s`hVbUef#f7Xx#IY+v{n5FDNQkzG#!A#k6@Bp&XH$H zZP`FnZ5)^$619OEk^gPvG_dMMA4tYW1^OVuQ)L~-S;{;q#5D5BnBn~{VF1OwCqSct zc8@oeBD!}DYvy3XvN%*h81`7PQ#w2KUN(h6o~(pb+diPEORXA6HWqT1eP+-7W616v zZlg`9W0cmjmmL>wzv>~@`nx?J-j(A%z!n?W2Ut;r2czHmU;1Igox4$+(|X?_;e-A~ zlj65wXaKNz5E+Ts`VAA^S@E5wSwNg~Em~75p6%?XnE+G{cpP_k@QG?~I*q92OHxnp1aX!`rAYeMPU2{|V$og!RI!Z*OHh0?QQCV>Ot`7aTeU9*K9( z46Ch}!_)grKc5{uSmwlIFQ^QRGc*(W2L@Q>y(7B{+%D%X*Z|=dvfS8yq ztTYIGEmNP8$)oOsS#Rq8ECsbG668{rm&qG=`#vv)h7S*X;95=^@B)lIZd7i;l_>AQ z)pOLS&Ra-xN2@z5yzQV zJ+lZkE#WR5GPGtqIipm|r576(zr8{jV03%_xDcenv!WtLs<}jzFBvTk`A)O(S-t_S z4(vKskk#njelNepRPd^5#5qy;wicJQ?{EO?Pt7PArC#uSI`t&+m6rP3LEfn@s8 z^OmDkktthyW_do+mhW;Kqf#z?!Gom|>H`987!OP45M2GqhgD9Cehc_{TG+38vHkf7ZsMg;;K=QP76Ai=x@1?P1c75&oi_REWdX@ zW5M0fyby(xENAG}?o8GLlUIBc(a?r5RuJYwvEvNv)X^6q*~|wGOfT zcnCfv;+p$^g>M@d3g z4~*JPG+wnZC0nEq?M9JinG-*4lHT$CdVY``{^qghZffxn)X*?ti`4IC3HG~zUruZ{ zrnVl~ed+gqYQ8f>vnp&f+|cn>7WFR&0-AH?@B9jbI{9F}`MWCVUjdBwBXQEsYK-Sp zKAdhCyltpr@?X6QKo{M9E{(MtPkcDbun}$#4x|$$lUfTg9U@aa%&7z z>MsUNS3^k5Ut7cf!@)eTire1E>Lu40N&Rjxr#V<;M z=!N@se+;f`y-Bw_kzXvvWOdDbsVSvqlWAG>dh0oh(Qz7$QOX0nB3)qsi0+Rcgja|5 z^Z&hzMn2jBV#oEU%-#^(yCJf7$~^elj`KtD&`V@R?2oL}enX=1fHrwHA()spE4N|3 zKP|qzfhO-TwXO;JDxfUV&-NKsZOGQ^?aJ3H`gykdE)IgJyw{l*nD5gp!NgmkCyAq% ziZE*Miplxs&(+@rz?sBDNvV;yAG-YXj9D?{){pKErRexOoKk#^-=4|%8Y$+SPb|#C zuv>9;$b9uh@`fDYseNk7-?1Q}*qwOvPWXVIM8p})ij1`DMttqC;!dMoH3S#toY9@epDuM)UrU4fMwTD z@nMBN;4)&sANv75pxph2FJ&-4){2W6tY~6>@k6h-`iTOgXQ6i99rHD}LK%4O$fjSq zbsVD5ztX+K>tH#cK1fE)(KP04pa;kYCxWaBZ%?O&uCk%Zb;o~rLF_XE~ zG@7gk{CAHPCw3koV4!yS;!d zlZB*38|Wc2ZssO8>}{|f7@T2@+WG9&@+PuCeVwR$pd{JPW$OHFu!(ti@hB2{_!hkA zbZ>VaVUUaqe4NnQRD4hBXlXqyz$q4e`_+T ze`4{ww3=z(UViI?6#prStAZHAWtxhCYWenq55Oj_5fZKX;A1blZ&KZ{wG1ng={2kf z_?w@Nk*sB-v^eBJWETVPsFQRoPScvYoQ;*`>4@ihuYQAt-Z%qn$p<_Hz3&mxjX3Nz zHCdMVUWXBuU22uU{FLWM6)b+px_u09;=@sB@0qo(?&EkypgxB&SI2yLiss7rm19nY zG7pX^^bH@cs;89~cfq^QcB{`)bn656zyE)^*Gnv#Vl705Np3#_Pp@zP+ILd-0q2&s z{07^mp+w4x-EU?t3iK23+yNLT|6` z*ZtL8X=$vef$?kM$-LEwfc2+WnG=@^7JVuziMTA;5srx_`p#mh^d2Z2lb>$u6{0CU zHaKJ01^6OD?VjYh-&*erJOG4#q19_@UtTiz)ozbHH>0XyCz@+ziL{Doc^2FyF)f~U4G?* zaQ8TWHlx4gY&aeqeb(GBYZE&aSLm9O)p$PWM1+?Q`V_S_0jECMPnnCQ4j7%AS%z}w zZY5_2YFnbD%SsbSs?bmYjO2H$N3cdj?f@sVwmIkZ$}ZN0 zs55VA+}V(+QNdmNq_Vv15PlYHZ?S@XStcz~^h?N&!!tjX%BmeR0%9o*qb*jq66m-T zm7W6bUMp%~#|FPzQ)xibMqws70zQd+1Qs0t`2C;`xVOTmx^K8^c9H3$-Jz(l**x*D zK?3$%(|CW)0saJdSm@$ajo89lYk6ngfCO#R*x6v+TWt@rwTiH{1jb4l*Zug#u!@AG zQE@=$H;m-dKj$+8#q~VhqfW$I1b5it;Li)a`d2T^I^oeK*WjU&m`I|#&WgqB(hi4F zkBjUXDB_tKn&Oo&_2>2Acu<;;Cx5Idju&{j>ze_Z#Oay{Mn_i1>ZHcG52l>?ji4Ih zp)5+P_CU?agw@z9^d)Zx)mo(vomiE_w_ht%doM^V5~r^nYgWlKhB=y-q^BJ@JgF2^ zW!bE*__MA1>Z!TTL6qmX^aLwThce3YTs}(9-~(d)yxTD8%r|EfYz6G@YxVt|^&9E3 z=m7nn1l{YFV-@1Fae^M$(4f4p+BJ&$GMlJl+67V!Hq}if&r2+4e$*;3IlY5otitB# zlBG7|J%I$`@8}Vhh7Vd<&(f-TCd47(Amq*%PI5O8vqWtg1w2CRCsIIv;(%f`r; zShc|%$!{U@XHxs`hReHGE@pcTmgOgFQ*-nNpf+9veC9 zoc?6EoLR)AZ~81G_tO(9PK@CTX#A>LxTle+Z?27dJ^>kDXjGq?L+}?!PitWDedYQ} z*oBDH8Y^sguGo1ih_p^`)U2Ng&=&>X_8)$iP@UNG+fJ9r_K4#j&2+&j#expeN;Ppu zO53;Q2q0Z^GdF`emM67$7vwuFAJgNDR6ghF;OseFRmE>sElg3NzXvHRSQLc%(5;dW zmn|`ZRHOsXy4_qG-@+6;v^^X$yEHD(O7xtC8W6k}PJi}QZxN3it@ z4Btc?{-dxFjwT(gr}>YtvQiaJTjW8$&Z}mFZ)b!Al^=n6q9-o&I7|yc@y#i#7 z^sVU)8)@{Xe0jM@6NQEfV`@|7)ZfB>(1=FwotoF%Fa@uBB!(=W9X4)Uyc8DJMk}<8 zX=tMnS4LUKM_9_jv9XM)%Jb>f8~xxm66DI%G)g6osBkrNlx*W%a{H-!T`SMgY}_ zgX(uQJD}XZcq6L(($mjp9&~-I+Z1XZjW6G}Fq%(2j`s_q;=k!mvYwq~KiTOFvZv1> z!+og!mWO%KRv;c+5GD&>eb#4v)qk?L$X#SMeQnznW|uyc`*=XAbm}ns@d8ZiN&BiK0>S_wO8gE;_ysT^dtgvXwyWsm`!dDt^S&z;13=$UV6hys<3B0Gv z%wFlmQfw-jm@RC>Hj>M%#Uc8Eb6c04dYudbc^DdT4d zTuptl>vjeVt$F3JVP)_?dh9CGV!FOzLIaiEvm^2gP?L-RVdHlc%)|5W)|rru_6^!t zP)#pnA6;>9yOT# zQ+ptA(YmgYRh_$!+L0bE*49_{pt>64!H;%tXjLg!+W4An)T-Nw@^Juxrdzb@zNAP!j5NDxs$1%qakp&I{dXK@! z^)|2m)BNAUM4?Mu{}Q%RZh2`f+`RA9ox-*oUU4LsdSArZa5C{ypv?IwUg2qC(T3xK zAVY%RPQe0Hl>5$+W$kClPB+6xab$MloP^C<11oy$OXS*0JHX87ji zEbFvjqL0359C-R!7p)`-%UGY4#FDg@JuSO30(fO8Q!F-*zewGsF|4SqzmfUzi_NRB zr{G*{Etz&%!TrWKTtZ5T@{B_J-i$`no_fPsaD#2$I5>j8c^ZXwgooS#C<*#Gz!~X& zTFHYNHh-hu0R{5&{Q&&F_t^-Y+$0Gat)@4I)NQreZRmrz(13NAvj9AmS^tQ+n10Ua ze|1DvzS!~YEoTx)w1_^GS=zrGZLKE5ND^A984p6zWB_WDcHX zDj$OizRV=!Hb(v48JByFd~8qYcOxq4(Z~Vk^@BehRt;5ak1V`IP!YPCxYJ|Zq|y&DG_PnC{i^* zObnI}^JEvTlF3Ql?yLZUsi-JK%_!dyuxt#6v@oXB^Jf1(ks=i_m?7_G(nJED489jd=zuttKD8LOf%cS;5OyGC)^a@Iv3Awjkul=%rDpic7Yk=rKCTmYwtH2epokJ&eN$ z$+$s*#KPB`mSjb`QVg+mVX}3%*07+CE>ok3*LlIbc7XCe(ezHSg^-8`g zBWzqhfv0%SCO^&+2N|@yuC1zeDj>qqNe3$Tx}Hp+vXMP1;?#hihSb?F?KftvD|)qP_%AbXMa6(9E-ra+^1p0`kGfveF7&ZaF_gl7#tgvMaN>RuGuMX=$E4eA|J zy{p#H6JS<1%JV83cv+^)Z(?4r-FG7=GH#hnn3BlqO;07H@=<*&MH zrCV;W-^(oM@5bEz`wu~Sh4Vr$ds?x?X8i_%%2&nlyEv|C02%?Lh@ud?#%M>~0n8sC zYKEu&V(}jk$9P`)LfhD|$xoj-K+QjuO=(1S&f(n(Lu~nA{iQ8Tt3&@{^$5@qE<11~ zDcPirku|8u!@y|iN?Y&KB{>;&4o?a7G|?ywJH7XSUDM>h>I$lBYUwL5CMSw6(~Um7 zG}02E!wV(<$pZdcS-^iUVQQiJb5?bTzwd0arH6G-?yM#+{oLV`N!nxC_~dIB?|(ph O^!1Fc75;tq@qYoLHm5EC diff --git a/assets/images/video-calling/video-compositor.png b/assets/images/video-calling/video-compositor.png deleted file mode 100644 index d24ff8a4fe0de891f1a1917388196ba2f57301be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63427 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 implementerAgoraClientClientToken ServerToken ServerClientClientAgora PlatformAgora PlatformSetup clientUser login to implementor securityAuthenticate and retrieve tokenUser login to implementor securityAuthenticate and retrieve tokenCreate client instanceSet client role: hostCreate client instanceSet client role: audienceRun callConnect to local audio and video resourcesJoin channelSend audio and video to channelJoin channelSend audio and video to channelCommunicateEnd callClose audio and videoLeave callClose audio and videoLeave call \ No newline at end of file diff --git a/assets/images/video-calling/video_call_workflow_run_end.svg b/assets/images/video-calling/video_call_workflow_run_end.svg new file mode 100644 index 000000000..ae2b2a5a8 --- /dev/null +++ b/assets/images/video-calling/video_call_workflow_run_end.svg @@ -0,0 +1 @@ +Call implementerAgoraClientClientClientClientAgora PlatformAgora PlatformRun callConnect to local audio and video resourcesJoin channelSend audio and video to channelConnect to local audio and video resourcesJoin channelSend audio and video to channelCommunicateEnd callClose audio and videoLeave channelClose audio and videoLeave channel \ No newline at end of file diff --git a/assets/images/video-sdk/agora_skin.iuml b/assets/images/video-sdk/agora_skin.iuml index dfb8fee7e..7ea66f8cc 100644 --- a/assets/images/video-sdk/agora_skin.iuml +++ b/assets/images/video-sdk/agora_skin.iuml @@ -3,73 +3,76 @@ scale max 1000 width skinparam linetype ortho hide stereotype -skinparam backgroundColor white +skinparam backgroundColor #00000000 skinparam Shadowing false -skinparam BoundaryBackgroundColor transparent +skinparam BoundaryBackgroundColor #00000000 skinparam BoundaryBorderColor #0a9efdff skinparam DiagramBorderColor #0a9efdff skinparam defaultTextAlignment center skinparam SequenceMessageAlign center skinparam ReferenceBorderThickness 2 + skinparam sequence { 'Users and boundries (DBS for example) ActorBorderThickness 3 ActorBorderColor #0a9efdff - ActorBackgroundColor transparent + ActorBackgroundColor #00000000 ActorFontColor #0a9efdff - ActorFontName Roboto - ActorFontStyle bold + ActorFontName Arial + ActorFontStyle regular ActorFontSize 14 - AgentBackgroundColor #0a9efdff + AgentBackgroundColor #00000000 'Arrows ArrowFontColor #0a9efdff ArrowFontSize 14 - ArrowFontStyle bold + ArrowFontStyle regular ArrowColor #ee4c14 - ArrowThickness 2 + ArrowThickness 2fs RectangleBorderThickness 3 - BoxBackgroundColor #f3f3f3 + BoxBackgroundColor #99999955 BoxBorderColor #0a9efdff 'Capability BoxFontColor #0a9efdff - BoxFontName Roboto - BoxFontStyle bold + BoxFontName Arial + BoxFontStyle regular BoxFontSize 16 BoundaryBackgroundColor #0a9efdff BoundaryBorderColor #0a9efdff BoundaryFontColor #0a9efdff - BoundryFontName Roboto + BoundryFontName Arial BoundaryStereotypeFontColor #0a9efdff ClassBorderColor #0a9efdff ComponentBorderColor #0a9efdff - DividerBackgroundColor white + DividerBackgroundColor #00000000 DividerBorderColor #0a9efdff DividerBorderThickness 3 DividerFontColor #0a9efdff - DividerFontName Roboto - DividerFontSize bold + DividerFontName Arial + DividerFontSize regular DividerFontStyle 14 ' Loops but not the title text GroupBorderColor #0a9efdff + GroupBodyBackgroundColor #0a9efd11 + GroupHeaderBackgroundColor #0a9efd11 GroupBorderThickness 2 - GroupFontName Roboto - GroupFontSize 14 + GroupFontName Arial + GroupFontSize 13 GroupFontColor #0a9efdff 'Title text for loops - GroupHeaderFontColor #0a9efdff - GroupHeaderFontName Roboto - GroupHeaderFontSize 14 - GroupHeaderFontStyle bold + GroupHeaderFontColor #000000 + GroupHeaderFontName Arial + GroupHeaderFontSize 13 + GroupHeaderFontStyle regular LifeLineBorderColor #0a9efdff LifeLineBackgroundColor #0a9efdff @@ -77,10 +80,10 @@ skinparam sequence { 'Services ParticipantBorderColor #0a9efdff - ParticipantBackgroundColor white + ParticipantBackgroundColor #00000000 ParticipantFontSize 14 - ParticipantFontStyle bold - ParticipantFontName Roboto + ParticipantFontStyle regular + ParticipantFontName Arial ParticipantFontColor #0a9efdff ParticipantBorderThickness 2 @@ -90,37 +93,37 @@ skinparam sequence { ' SwimlaneBorderThickness 2 TitleFontColor #0a9efdff - TitleFontName Roboto + TitleFontName Arial TitleFontSize 18 TitleBorderThickness 5 } skinparam interface { - BackgroundColor #FFFFFF + BackgroundColor #00000000 BorderColor #0a9efdff BorderThickness 1 FontColor #0a9efdff - FontName Roboto - FontSize 14 - FontStyle bolds + FontName Arial + FontSize 13 + FontStyle regulars } skinparam usecase { - BackgroundColor #FFFFFF + BackgroundColor #00000000 BorderColor #ee4c14 BorderThickness 1 FontColor #0a9efdff - FontName Roboto - FontSize 14 - FontStyle bold + FontName Arial + FontSize 13 + FontStyle regular } skinparam note { - BackgroundColor #FFFFFF + BackgroundColor #00000000 BorderColor #0a9efdff BorderThickness 1 @@ -130,48 +133,48 @@ skinparam note { EndColor #0a9efdff FontColor #0a9efdff - FontName Roboto - FontSize 14 - FontStyle bold + FontName Arial + FontSize 13 + FontStyle regular } skinparam activity { - BackgroundColor #FFFFFF - DiamondBackgroundColor #FFFFFF + BackgroundColor #00000000 + DiamondBackgroundColor #00000000 DiamondBorderColor #0a9efdff BorderColor #0a9efdff BorderThickness 4 - DiamondBackgroundColor #FFFFFF + DiamondBackgroundColor #00000000 DiamondBorderColor #0a9efdff DiamondFontColor #0a9efdff - activityDiamondFontName Roboto - activityDiamondFontSize 14 - DiamondFontStyle bold + activityDiamondFontName Arial + activityDiamondFontSize 13 + DiamondFontStyle regular StartColor #0a9efdff StopColor #0a9efdff BarColor #0a9efdff EndColor #0a9efdff FontColor #0a9efdff - FontName Roboto - FontSize 14 - FontStyle bold + FontName Arial + FontSize 13 + FontStyle regular } skinparam card { - BackgroundColor transparent - BorderColor transparent + BackgroundColor #00000000 + BorderColor #00000000 BorderThickness 0 - FontColor transparent + FontColor #00000000 } skinparam rectangle { - BackgroundColor #FFFFFF + BackgroundColor #00000000 BorderColor #0a9efdff BorderThickness 4 @@ -181,16 +184,16 @@ skinparam rectangle { EndColor #0a9efdff FontColor #0a9efdff - FontName Roboto - FontSize 14 - FontStyle bold + FontName Arial + FontSize 13 + FontStyle regular shadowing true } skinparam agent { - BackgroundColor #FFFFFF + BackgroundColor #00000000 BorderColor #0a9efdff BorderThickness 4 @@ -199,16 +202,16 @@ skinparam agent { EndColor #0a9efdff FontColor #0a9efdff - FontName Roboto - FontSize 14 - FontStyle bold + FontName Arial + FontSize 13 + FontStyle regular shadowing true } skinparam folder { - BackgroundColor #FFFFFF + BackgroundColor #00000000 BorderColor #0a9efdff BorderThickness 4 @@ -218,16 +221,16 @@ skinparam folder { EndColor #0a9efdff FontColor #0a9efdff - FontName Roboto - FontSize 14 - FontStyle bold + FontName Arial + FontSize 13 + FontStyle regular shadowing true } skinparam file { - BackgroundColor #FFFFFF + BackgroundColor #00000000 BorderColor #0a9efdff BorderThickness 4 @@ -237,16 +240,16 @@ skinparam file { EndColor #0a9efdff FontColor #0a9efdff - FontName Roboto - FontSize 14 - FontStyle bold + FontName Arial + FontSize 13 + FontStyle regular shadowing true } skinparam cloud { - BackgroundColor #FFFFFF + BackgroundColor #00000000 BorderColor #0a9efdff BorderThickness 4 @@ -256,15 +259,15 @@ skinparam cloud { EndColor #0a9efdff FontColor #0a9efdff - FontName Roboto - FontSize 14 - FontStyle bold + FontName Arial + FontSize 13 + FontStyle regular } skinparam class { - BackgroundColor #FFFFFF + BackgroundColor #00000000 BorderColor #0a9efdff BorderThickness 1 @@ -274,9 +277,9 @@ skinparam class { EndColor #0a9efdff FontColor #0a9efdff - FontName Roboto - FontSize 14 - FontStyle bold + FontName Arial + FontSize 13 + FontStyle regular @@ -285,15 +288,15 @@ skinparam class { 'For services skinparam component { - BackgroundColor #FFFFFF + BackgroundColor #00000000 BorderColor #0a9efdff BorderThickness 2 FontColor #0a9efdff - FontName Roboto - FontSize 14 - FontStyle bold + FontName Arial + FontSize 13 + FontStyle regular } @@ -301,15 +304,15 @@ skinparam component { 'databases skinparam database { - BackgroundColor #f3f3f3 + BackgroundColor #88888855 BorderColor #0a9efdff BorderThickness 2 FontColor #0a9efdff - FontName Roboto - FontSize 14 - FontStyle bold + FontName Arial + FontSize 13 + FontStyle regular } @@ -319,14 +322,14 @@ skinparam frame { ArrowColor #0a9efdff - BackgroundColor #f3f3f3 + BackgroundColor #00000000 BorderColor #0a9efdff BorderThickness 4 FontColor #0a9efdff - FontName Roboto - FontSize 14 - FontStyle bold + FontName Arial + FontSize 13 + FontStyle regular } @@ -336,51 +339,50 @@ skinparam node { ArrowColor #0a9efdff BorderThickness 2 - BackgroundColor #FFFFFF + BackgroundColor #00000000 BorderColor #0a9efdff FontColor #0a9efdff - FontName Roboto - FontSize 14 - FontStyle bold + FontName Arial + FontSize 13 + FontStyle regular } skinparam package { BorderThickness 2 - BackgroundColor #FFFFFF + BackgroundColor #00000000 BorderColor #0a9efdff - PackageStereotypeFontColor #0a9efdff - packageFontColor #0a9efdff - FontName Roboto - FontSize 18 - FontStyle italic -} + FontColor #0a9efdff + FontName Arial + FontSize 13 + FontStyle regular +} ' Activity diagrams, for different capabilities skinparam partition { - BackgroundColor #f3f3f3 + BackgroundColor #88888855 BorderColor #0a9efdff BorderThickness 2 FontColor #0a9efdff - FontName Roboto - FontSize 14 - FontStyle bold + FontName Arial + FontSize 13 + FontStyle regular } skinparam ArrowFontColor #0a9efdff -skinparam ArrowFontSize 14 -skinparam ArrowFontStyle bold +skinparam ArrowFontSize 13 +skinparam ArrowFontStyle regular skinparam ArrowColor #0a9efdff skinparam ArrowThickness 2 -skinparam ArrowFontName Roboto +skinparam ArrowFontName Arial skinparam linetype ortho hide stereotype @@ -401,7 +403,3 @@ rectangle "<$e_sprite>\n\n==e_label" <> as e_al !definelong AWSEntity(e_alias, e_label, e_techn, e_descr, e_color, e_sprite, e_stereo) rectangle "<$e_sprite>\n\n==e_label" <> as e_alias !enddefinelong - - - - diff --git a/assets/images/video-sdk/audio-and-voice-effects-web.puml b/assets/images/video-sdk/audio-and-voice-effects-web.puml new file mode 100644 index 000000000..6412f5b39 --- /dev/null +++ b/assets/images/video-sdk/audio-and-voice-effects-web.puml @@ -0,0 +1,41 @@ +@startuml audio-and-voice-effects +!include agora_skin.iuml + +actor "User" as USR + +box "Your app" +participant "Video SDK" as APP +end box + +box "Agora" +participant "SD-RTN" as API +end box + +USR -> APP: Open App +APP -> APP: Create an Agora Engine instance using Video SDK +APP -> APP: Enable audio and video in Agora Engine + +group Join +USR -> APP: Join a channel +APP -> APP: Retrieve authentication token to join a channel +APP -> API: Join the channel +end + +group Audio mixing +USR -> APP: Select an audio file +APP -> API: Proccess the audio file +USR -> APP: Start audio mixing +APP -> API: Play the audio file +USR -> APP: Stop mixing +APP -> API: Stop the audio file +end + +group Change audio route +APP -> API: Set default audio route +APP -> API: Change the audio route temporarily +end + +USR -> APP: Leave the channel +APP -> API: Leave the channel + +@enduml diff --git a/assets/images/video-sdk/audio-and-voice-effects-web.svg b/assets/images/video-sdk/audio-and-voice-effects-web.svg new file mode 100644 index 000000000..cb72696ec --- /dev/null +++ b/assets/images/video-sdk/audio-and-voice-effects-web.svg @@ -0,0 +1 @@ +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppCreate an Agora Engine instance using Video SDKEnable audio and video in Agora EngineJoinJoin a channelRetrieve authentication token to join a channelJoin the channelAudio mixingSelect an audio fileProccess the audio fileStart audio mixingPlay the audio fileStop mixingStop the audio fileChange audio routeSet default audio routeChange the audio route temporarilyLeave the channelLeave the channel \ No newline at end of file diff --git a/assets/images/video-sdk/audio-and-voice-effects.svg b/assets/images/video-sdk/audio-and-voice-effects.svg index f98dfbd3c..e4acdf7ad 100644 --- a/assets/images/video-sdk/audio-and-voice-effects.svg +++ b/assets/images/video-sdk/audio-and-voice-effects.svg @@ -1,496 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppCreate an Agora Engine instance using Video SDKEnable audio and video in Agora EngineJoinJoin a channelRetrieve authentication token to join a channelJoin the channelPlay audio effectTrigger sound effectPlay effectPause and resume effectSet effect positionSet effect volumeOn audio effect finishedAudio mixingControl audio mixingStart audio mixingStop audio mixingVoice effectsApply a voice effectSet preset voice effectDisable the voice effectChange audio routeSet default audio routeChange the audio route temporarilyLeave the channelLeave the channel \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppCreate an Agora Engine instance using Video SDKEnable audio and video in Agora EngineJoinJoin a channelRetrieve authentication token to join a channelJoin the channelPlay audio effectTrigger sound effectPlay effectPause and resume effectSet effect positionSet effect volumeOn audio effect finishedAudio mixingControl audio mixingStart audio mixingStop audio mixingVoice effectsApply a voice effectSet preset voice effectDisable the voice effectLeave the channelLeave the channel \ No newline at end of file diff --git a/assets/images/video-sdk/authentication-logic.svg b/assets/images/video-sdk/authentication-logic.svg index 7e964a030..71c783045 100644 --- a/assets/images/video-sdk/authentication-logic.svg +++ b/assets/images/video-sdk/authentication-logic.svg @@ -1,450 +1 @@ -Implemented by youProvided by AgoraToken serverToken serverAppAppSD-RTNSD-RTNJoin a channel with authenticationRequest a token using channel name,role, token type, and user IDValidate user againstinternal securityGenerate a token and return it to the clientJoin a channel with uid,channel name, and tokenValidatethe tokenTrigger the callback afteradding user to the channelRenew tokenTrigger event: Token Privilege will ExpireRequest a fresh token using channel name,role, token type, and user IDValidate user request against internal logicGenerate a fresh token and return it to the clientSend the fresh tokenwith a RenewToken request \ No newline at end of file +Implemented by youProvided by AgoraToken serverToken serverAppAppSD-RTNSD-RTNJoin a channel with authenticationRequest a token using channel name,role, token type, and user IDValidate user againstinternal securityGenerate a token and return it to the clientJoin a channel with uid,channel name, and tokenValidatethe tokenTrigger the callback afteradding user to the channelRenew tokenTrigger event: Token Privilege will ExpireRequest a fresh token using channel name,role, token type, and user IDValidate user request against internal logicGenerate a fresh token and return it to the clientSend the fresh tokenwith a RenewToken request \ No newline at end of file diff --git a/assets/images/video-sdk/cloud-proxy.svg b/assets/images/video-sdk/cloud-proxy.svg index 4b3bbbb34..b5328a643 100644 --- a/assets/images/video-sdk/cloud-proxy.svg +++ b/assets/images/video-sdk/cloud-proxy.svg @@ -1 +1 @@ -Implemented by youProvided by AgoraUserUserAdminAdminAppAppEnterprise FirewallEnterprise FirewallCloud ProxyCloud ProxySD-RTNSD-RTNWhitelist IP addresses and portsfor Cloud Proxy in the firewall.Open the appInitialize the Agora VideoSDK engineJoin a channelalt[Join a channel directly]Join a ChannelJoin SuccessSend and receive data[Join Channel failed]Video SDK automatically attempts to connect securely on TLS 443Join SuccessSend and receive data[Connection attempt on TLS 443 failed: Enable cloud proxy]Call the method to enablea Cloud Proxy connectionRequest access toCloud ProxyCheck whitelist to grantaccessRequest access toCloud ProxyProxy informationJoin a channelRequest to join a channelJoin successJoin successSend data streamSend and receive data streamReceive data stream \ No newline at end of file +Implemented by youProvided by AgoraUserUserAdminAdminAppAppEnterprise FirewallEnterprise FirewallCloud ProxyCloud ProxySD-RTNSD-RTNWhitelist IP addresses and portsfor Cloud Proxy in the firewall.Open the appInitialize the Agora VideoSDK engineJoin a channelalt[Join a channel directly]Join a ChannelJoin SuccessSend and receive data[Join Channel failed]Video SDK automatically attempts to connect securely on TLS 443Join SuccessSend and receive data[Connection attempt on TLS 443 failed: Enable cloud proxy]Call the method to enablea Cloud Proxy connectionRequest access toCloud ProxyCheck whitelist to grantaccessRequest access toCloud ProxyProxy informationJoin a channelRequest to join a channelJoin successJoin successSend data streamSend and receive data streamReceive data stream \ No newline at end of file diff --git a/assets/images/video-sdk/custom-source-video-audio.svg b/assets/images/video-sdk/custom-source-video-audio.svg index 12305b496..be688a7ed 100644 --- a/assets/images/video-sdk/custom-source-video-audio.svg +++ b/assets/images/video-sdk/custom-source-video-audio.svg @@ -1,470 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppCreate an instance of the Video SDK engineEnable audio and video in the engineSetup external sourceCheck the external source for compatibilitySet external video or audio sourceJoinJoin a channelRetrieve authentication token to join a channelJoin the channelProcess dataManage capturing and processingusing external methodsStream dataPush external video or audio frameLeave the channelLeave the channel \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppCreate an instance of the Video SDK engineEnable audio and video in the engineSetup external sourceCheck the external source for compatibilitySet external video or audio sourceJoinJoin a channelRetrieve authentication token to join a channelJoin the channelProcess dataManage capturing and processingusing external methodsStream dataPush external video or audio frameLeave the channelLeave the channel \ No newline at end of file diff --git a/assets/images/video-sdk/ensure-channel-quality.svg b/assets/images/video-sdk/ensure-channel-quality.svg index cf56f4d12..af0a3cfc9 100644 --- a/assets/images/video-sdk/ensure-channel-quality.svg +++ b/assets/images/video-sdk/ensure-channel-quality.svg @@ -1,480 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppCreate Agora engineSet log file path, log level and file sizeCreate Agora EngineBest practice for app initiationEnable dual-stream modeto allow remote users to choose a stream typeSet local publish and remote subscribe fallback optionsSettings checkSpecify audio profile and scenariobased on the nature of the appSet video encoder configurationCall the method to start the network probe testDeliver network quality scoreand network statisticsJoin channelEnable videoJoin channelIn-call quality monitoringEnable the quality statisticsReceive network, call, audio and video quality statisticsReceive state change notificationsNotify the userTake corrective action \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppCreate Agora engineSet log file path, log level and file sizeCreate Agora EngineBest practice for app initiationEnable dual-stream modeto allow remote users to choose a stream typeSet local publish and remote subscribe fallback optionsSettings checkSpecify audio profile and scenariobased on the nature of the appSet video encoder configurationCall the method to start the network probe testDeliver network quality scoreand network statisticsJoin channelEnable videoJoin channelIn-call quality monitoringEnable the quality statisticsReceive network, call, audio and video quality statisticsReceive state change notificationsNotify the userTake corrective action \ No newline at end of file diff --git a/assets/images/video-sdk/ils-call-logic-android.svg b/assets/images/video-sdk/ils-call-logic-android.svg index 5a79b96f7..92170cfb3 100644 --- a/assets/images/video-sdk/ils-call-logic-android.svg +++ b/assets/images/video-sdk/ils-call-logic-android.svg @@ -1,490 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Agora Video SDK engine:agoraEngine = RtcEngine.createEnable the video module:agoraEngine.enableVideo()Set the channel profile:agoraEngine.setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING)HostStart a live streaming eventRetrieve authentication token to join channelSet the role as host:agoraEngine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER)Setup local video:agoraEngine.setupLocalVideo(VideoCanvas)Start local preview:agoraEngine.startPreview()Join a channel as host:agoraEngine.joinChannelSend data streamAudienceJoin a live streaming eventRetrieve authentication token to join channelSet the user role as audience:agoraEngine.setClientRole(CLIENT_ROLE_AUDIENCE)Join the channel:agoraEngine.joinChannelRetrieve streaming from the hosts:agoraEngine.setupRemoteVideoReceive data streamLeave the live streaming eventLeave the channel:agoraEngine.leaveChannel()Close appClean up local resourcesagoraEngine.destroy() \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Agora Video SDK engine:agoraEngine = RtcEngine.createEnable the video module:agoraEngine.enableVideo()Set the channel profile:agoraEngine.setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING)HostStart a live streaming eventRetrieve authentication token to join channelSet the role as host:agoraEngine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER)Setup local video:agoraEngine.setupLocalVideo(VideoCanvas)Start local preview:agoraEngine.startPreview()Join a channel as host:agoraEngine.joinChannelSend data streamAudienceJoin a live streaming eventRetrieve authentication token to join channelSet the user role as audience:agoraEngine.setClientRole(CLIENT_ROLE_AUDIENCE)Join the channel:agoraEngine.joinChannelRetrieve streaming from the hosts:agoraEngine.setupRemoteVideoReceive data streamLeave the live streaming eventLeave the channel:agoraEngine.leaveChannel()Close appClean up local resourcesagoraEngine.destroy() \ No newline at end of file diff --git a/assets/images/video-sdk/ils-call-logic-electron.svg b/assets/images/video-sdk/ils-call-logic-electron.svg index 109593b4b..7edcfac84 100644 --- a/assets/images/video-sdk/ils-call-logic-electron.svg +++ b/assets/images/video-sdk/ils-call-logic-electron.svg @@ -1,482 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppCreate an instance of the Video SDK engine:agoraEngine = agoraEngine.createAgoraRtcEngineInitialize the created instance:agoraEngine.initializeSetup the callback functions:agoraEngine.registerEventHandlerSet the channel profile:agoraEngine.setChannelProfileHostStart live streaming eventSetup local video:agoraEngine.setupLocalVideoEnable local video capturer:agoraEngine.enableVideoStart local preview:agoraEngine.startPreviewSet the user role as host:agoraEngine.setChannelProfile(ChannelProfileType.ChannelProfileLiveBroadcasting)Retrieve authentication token to join channelJoin a channel as host:agoraEngine.joinChannelSend data streamAudienceJoin live streaming eventRetrieve authentication token to join channelSet the user role as audience:agoraEngine.setChannelProfile(ClientRoleType.ClientRoleAudience)Join the live streaming event:agoraEngine.joinChannelRetrieve streaming from the other user:agoraEngine.setupRemoteVideoLeave live streaming eventagoraEngine.leaveChannel \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppCreate an instance of the Video SDK engine:agoraEngine = agoraEngine.createAgoraRtcEngineInitialize the created instance:agoraEngine.initializeSetup the callback functions:agoraEngine.registerEventHandlerSet the channel profile:agoraEngine.setChannelProfileHostStart live streaming eventSetup local video:agoraEngine.setupLocalVideoEnable local video capturer:agoraEngine.enableVideoStart local preview:agoraEngine.startPreviewSet the user role as host:agoraEngine.setChannelProfile(ChannelProfileType.ChannelProfileLiveBroadcasting)Retrieve authentication token to join channelJoin a channel as host:agoraEngine.joinChannelSend data streamAudienceJoin live streaming eventRetrieve authentication token to join channelSet the user role as audience:agoraEngine.setChannelProfile(ClientRoleType.ClientRoleAudience)Join the live streaming event:agoraEngine.joinChannelRetrieve streaming from the other user:agoraEngine.setupRemoteVideoLeave live streaming eventagoraEngine.leaveChannel \ No newline at end of file diff --git a/assets/images/video-sdk/ils-call-logic-flutter.svg b/assets/images/video-sdk/ils-call-logic-flutter.svg index c2b7f6661..926fd8899 100644 --- a/assets/images/video-sdk/ils-call-logic-flutter.svg +++ b/assets/images/video-sdk/ils-call-logic-flutter.svg @@ -1,486 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitialize the Agora Video SDK engine:agoraEngine = createAgoraRtcEngine()Enable the video module:agoraEngine.enableVideoRegister the event handler:agoraEngine.registerEventHandlerSetup AgoraVideoView widgetfor local or remote videoHostStart a live streaming eventSet the client role as Host:agoraEngine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);Set a channel profile:.setChannelProfile(ChannelProfileType.channelProfileLiveBroadcasting);Retrieve authentication tokenJoin a channel using the token:agoraEngine.joinChannelSend data streamsRemote user joined:RtcEngineEventHandler onUserJoined:Start local peview:agoraEngine.startPreview()Display local video using AgoraVideoViewAudienceJoin a live streaming eventSet the client role as Audience:agoraEngine.setClientRole(role: ClientRoleType.clientRoleAudience);Set a channel profile:.setChannelProfile(ChannelProfileType.channelProfileLiveBroadcasting);Retrieve authentication tokenJoin a channel using the token:agoraEngine.joinChannelRemote user joined:RtcEngineEventHandler onUserJoined:Receive data streamsRender remote video using AgoraVideoViewLeave broadcastagoraEngine.leaveChannel()Close app \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitialize the Agora Video SDK engine:agoraEngine = createAgoraRtcEngine()Enable the video module:agoraEngine.enableVideoRegister the event handler:agoraEngine.registerEventHandlerSetup AgoraVideoView widgetfor local or remote videoHostStart a live streaming eventSet the client role as Host:agoraEngine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);Set a channel profile:.setChannelProfile(ChannelProfileType.channelProfileLiveBroadcasting);Retrieve authentication tokenJoin a channel using the token:agoraEngine.joinChannelSend data streamsRemote user joined:RtcEngineEventHandler onUserJoined:Start local peview:agoraEngine.startPreview()Display local video using AgoraVideoViewAudienceJoin a live streaming eventSet the client role as Audience:agoraEngine.setClientRole(role: ClientRoleType.clientRoleAudience);Set a channel profile:.setChannelProfile(ChannelProfileType.channelProfileLiveBroadcasting);Retrieve authentication tokenJoin a channel using the token:agoraEngine.joinChannelRemote user joined:RtcEngineEventHandler onUserJoined:Receive data streamsRender remote video using AgoraVideoViewLeave broadcastagoraEngine.leaveChannel()Close app \ No newline at end of file diff --git a/assets/images/video-sdk/ils-call-logic-ios.svg b/assets/images/video-sdk/ils-call-logic-ios.svg index b925b4213..1c32a1c60 100644 --- a/assets/images/video-sdk/ils-call-logic-ios.svg +++ b/assets/images/video-sdk/ils-call-logic-ios.svg @@ -1,484 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitiate the Video SDK engine:agoraEngine = AgoraRtcEngineKit.sharedEngineStart video in the engine:agoraEngine.enableVideo()HostStart a live streaming eventIn an live streaming event, only the hosts broadcast to the channel:agoraEngine.setClientRole(.broadcaster)Start local video:agoraEngine.setupLocalVideo(videoCanvas)Join the channel:agoraEngine?.joinChannelSend data streamAudienceJoin live streaming eventIn an live streaming event, the audience views the stream sent by channel hosts:agoraEngine.setClientRole(.audience)Join the channel:agoraEngine?.joinChannelRetrieve streaming from the hosts:agoraEngine.setupRemoteVideo(videoCanvas)Receive data streamsLeave live streaming eventStop local video:agoraEngine.stopPreview()Leave the channel:agoraEngine.leaveChannel(nil)Close appClean up local resources:AgoraRtcEngineKit.destroy() \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitiate the Video SDK engine:agoraEngine = AgoraRtcEngineKit.sharedEngineStart video in the engine:agoraEngine.enableVideo()HostStart a live streaming eventIn an live streaming event, only the hosts broadcast to the channel:agoraEngine.setClientRole(.broadcaster)Start local video:agoraEngine.setupLocalVideo(videoCanvas)Join the channel:agoraEngine?.joinChannelSend data streamAudienceJoin live streaming eventIn an live streaming event, the audience views the stream sent by channel hosts:agoraEngine.setClientRole(.audience)Join the channel:agoraEngine?.joinChannelRetrieve streaming from the hosts:agoraEngine.setupRemoteVideo(videoCanvas)Receive data streamsLeave live streaming eventStop local video:agoraEngine.stopPreview()Leave the channel:agoraEngine.leaveChannel(nil)Close appClean up local resources:AgoraRtcEngineKit.destroy() \ No newline at end of file diff --git a/assets/images/video-sdk/ils-call-logic-template.svg b/assets/images/video-sdk/ils-call-logic-template.svg index da5be5cdf..947f772d7 100644 --- a/assets/images/video-sdk/ils-call-logic-template.svg +++ b/assets/images/video-sdk/ils-call-logic-template.svg @@ -1,484 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitiate the Video SDK engine.Start video in the engine.HostStart ILS eventIn an ILS event, only the hosts broadcast to the channel.Start local video.Join the channel.Send data stream.AudienceJoin ILS eventIn an ILS event, the audience views the broadcast made by channel hosts.Join the channel.Retrieve streaming from the other user.Receive data streamsLeave ILS eventStop local video.Leave the channel.Close appClean up local resources. \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitiate the Video SDK engine.Start video in the engine.HostStart ILS eventIn an ILS event, only the hosts broadcast to the channel.Start local video.Join the channel.Send data stream.AudienceJoin ILS eventIn an ILS event, the audience views the broadcast made by channel hosts.Join the channel.Retrieve streaming from the other user.Receive data streamsLeave ILS eventStop local video.Leave the channel.Close appClean up local resources. \ No newline at end of file diff --git a/assets/images/video-sdk/ils-call-logic-unity.puml b/assets/images/video-sdk/ils-call-logic-unity.puml index 5b298984d..ffe987f20 100644 --- a/assets/images/video-sdk/ils-call-logic-unity.puml +++ b/assets/images/video-sdk/ils-call-logic-unity.puml @@ -17,26 +17,22 @@ end box USR -> APP: Open game APP -> APP: Create an RtcEngine instance: \n RtcEngine = Agora.Rtc.RtcEngine.CreateAgoraRtcEngine() -APP -> API: Set channel profile: \n RtcEngine.SetChannelProfile(CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING) -APP -> API: Set the context: \n RtcEngineContext context = new RtcEngineContext(_appID, 0, true, - CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING, - AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT) +APP -> API: Set channel profile: \n RtcEngine.SetChannelProfile(\n CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING) +APP -> API: Set the context: \n RtcEngineContext context = new RtcEngineContext(_appID, 0, true, \n CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING, \n AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT) APP -> APP: Initialize RtcEngine: \n RtcEngine.Initialize(context) group Host USR -> APP: Start a live streaming event APP -> APP: Enable the video module: \n RtcEngine.EnableVideo() -APP -> API: Set the user role as host: \n RtcEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER) +APP -> API: Set the user role as host: \n RtcEngine.SetClientRole(\n CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER) APP -> API: Join a channel as host: \n RtcEngine.JoinChannel() -APP -> API: Send data stream end group Audience USR -> APP: Join a live streaming event APP -> APP: Enable the video module: \n RtcEngine.EnableVideo() -APP -> API: Set the user role as audience: \n RtcEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_AUDIENCE) +APP -> API: Set the user role as audience: \n RtcEngine.SetClientRole(\n CLIENT_ROLE_TYPE.CLIENT_ROLE_AUDIENCE) APP -> API: Join the channel as audience: \n RtcEngine.JoinChannel() -APP -> API: Receive data stream from the hosts end USR -> APP: Leave call diff --git a/assets/images/video-sdk/ils-call-logic-unity.svg b/assets/images/video-sdk/ils-call-logic-unity.svg index 4d2bcc8a1..0d63d98a8 100644 --- a/assets/images/video-sdk/ils-call-logic-unity.svg +++ b/assets/images/video-sdk/ils-call-logic-unity.svg @@ -1,492 +1 @@ -Your gameAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen gameInitiate the Agora Video SDK engine:agoraEngine=IRtcEngine.GetEngine()Setup the local video stream:agoraEngine.EnableVideo()agoraEngine.EnableVideoObserver()HostStart a live streaming eventRetrieve authentication token to join channelEnable live streaming in the channel:agoraEngine.SetChannelProfile(CHANNEL_PROFILE.CHANNEL_PROFILE_LIVE_BROADCASTING)Set the user role as host:agoraEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER)Join a channel as host:agoraEngine.JoinChannelByKey()Send data streamAudienceJoin the live streaming eventRetrieve authentication token to join channelSet the user role as audience:agoraEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_AUDIENCE)Join the channel:agoraEngine.JoinChannelByKey()A callback to start remote video:onUserJoined()Retrieve streaming from the hosts:RemoteView.SetForUser(uid)Receive data StreamLeave the live streaming eventLeave the channel:agoraEngine.leaveChannel()Stop local video stream:agoraEngine.DisableVideo()Disable the video observer:agoraEngine.DisableVideoObserver()Close gameClean up local resources:agoraEngine.destroy() \ No newline at end of file +Your gameAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen gameCreate an RtcEngine instance:RtcEngine = Agora.Rtc.RtcEngine.CreateAgoraRtcEngine()Set channel profile:RtcEngine.SetChannelProfile(CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING)Set the context:RtcEngineContext context = new RtcEngineContext(_appID, 0, true,CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING,AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT)Initialize RtcEngine:RtcEngine.Initialize(context)HostStart a live streaming eventEnable the video module:RtcEngine.EnableVideo()Set the user role as host:RtcEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER)Join a channel as host:RtcEngine.JoinChannel()AudienceJoin a live streaming eventEnable the video module:RtcEngine.EnableVideo()Set the user role as audience:RtcEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_AUDIENCE)Join the channel as audience:RtcEngine.JoinChannel()Leave callLeave the channel:RtcEngine.LeaveChannel()Disable the video modules:RtcEngine.DisableVideo()Stop rendering the remote video:RemoteView.SetEnable(false)Stop rendering the local video:LocalView.SetEnable(false)Close gameClean up local resources:RtcEngine.Dispose() diff --git a/assets/images/video-sdk/ils-call-logic-unreal.puml b/assets/images/video-sdk/ils-call-logic-unreal.puml index ffe8caf57..a84331c52 100644 --- a/assets/images/video-sdk/ils-call-logic-unreal.puml +++ b/assets/images/video-sdk/ils-call-logic-unreal.puml @@ -47,4 +47,4 @@ USR -> APP: Close app APP -> APP: Clean up local resources\n agoraEngine->release() -@enduml +@enduml \ No newline at end of file diff --git a/assets/images/video-sdk/ils-call-logic-unreal.svg b/assets/images/video-sdk/ils-call-logic-unreal.svg index 54686f187..3f357f912 100644 --- a/assets/images/video-sdk/ils-call-logic-unreal.svg +++ b/assets/images/video-sdk/ils-call-logic-unreal.svg @@ -1 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Agora Video SDK engine:agora::rtc::ue::createAgoraRtcEngine()Enable the audio and video modules:agoraEngine.enableVideo()agoraEngine.enableAudio()Set the channel profile:RtcEngineContext contextcontext.channelProfile = CHANNEL_PROFILE_COMMUNICATION;HostStart a live streaming eventRetrieve authentication token to join channelSet the role as host:agoraEngine->setClientRole(CLIENT_ROLE_BROADCASTER)Setup local video:agoraEngine.setupLocalVideo(videoCanvas)Join a channel as host:agoraEngine->joinChannelSend data streamAudienceJoin a live streaming eventRetrieve authentication token to join channelSet the user role as audience:agoraEngine->setClientRole(CLIENT_ROLE_AUDIENCE)Join the channel:agoraEngine->joinChannelRetrieve streaming from the hosts:agoraEngine->setupRemoteVideoReceive data streamLeave the live streaming eventLeave the channel:agoraEngine->leaveChannel()Close appClean up local resourcesagoraEngine->release() +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Agora Video SDK engine:agora::rtc::ue::createAgoraRtcEngine()Enable the audio and video modules:agoraEngine.enableVideo()agoraEngine.enableAudio()Set the channel profile:RtcEngineContext contextcontext.channelProfile = CHANNEL_PROFILE_COMMUNICATION;HostStart a live streaming eventRetrieve authentication token to join channelSet the role as host:agoraEngine->setClientRole(CLIENT_ROLE_BROADCASTER)Setup local video:agoraEngine.setupLocalVideo(videoCanvas)Join a channel as host:agoraEngine->joinChannelSend data streamAudienceJoin a live streaming eventRetrieve authentication token to join channelSet the user role as audience:agoraEngine->setClientRole(CLIENT_ROLE_AUDIENCE)Join the channel:agoraEngine->joinChannelRetrieve streaming from the hosts:agoraEngine->setupRemoteVideoReceive data streamLeave the live streaming eventLeave the channel:agoraEngine->leaveChannel()Close appClean up local resourcesagoraEngine->release() \ No newline at end of file diff --git a/assets/images/video-sdk/ils-call-logic-web.svg b/assets/images/video-sdk/ils-call-logic-web.svg index 3bbb766c5..46e2f8dc1 100644 --- a/assets/images/video-sdk/ils-call-logic-web.svg +++ b/assets/images/video-sdk/ils-call-logic-web.svg @@ -1,484 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Video SDK engine:agoraEngine = AgoraRTC.createClientSet the required event listners:agoraEngine.on("user-published")agoraEngine.on("user-unpublished")HostStart live streaming eventRetrieve authentication token to join channelSet the user role as host:agoraEngine.setClientRole("host")Join a channel as host:agoraEngine.joinCreate local media tracks :AgoraRTC.createMicrophoneAudioTrackAgoraRTC.createCameraVideoTrackPush local media tracks to the channel:agoraEngine.publishStop the remote video and play the local video:rtc.localVideoTrack.playrtc.remoteVideoTrack.stopRetrieve streaming from the other user:agoraEngine.on("user-published")AudienceJoin live streaming eventRetrieve authentication token to join channelSet the user role as audience:agoraEngine.setClientRole("audience")Join the live streaming event:agoraEngine.joinRetrieve streaming from the other user:agoraEngine.on("user-published")agoraEngine.subscribeStop the local video and play the remote video:rtc.localVideoTrack.stoprtc.remoteVideoTrack.playReceive data streamLeave live streaming eventagoraEngine.leave \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Video SDK engine:agoraEngine = AgoraRTC.createClientStart video in the engine:App.initStart local media:stream = AgoraRTC.createStreamstream.initstream.playHostStart live streaming eventRetrieve authentication token to join channelSet the user role as host:agoraEngine.setClientRole("host")Join a channel as host:agoraEngine.joinPush local media to the channel:agoraEngine.publishAudienceJoin live streaming eventRetrieve authentication token to join channelSet the user role as audience:agoraEngine.setClientRole("audience")Join the live streaming event:agoraEngine.joinRetrieve streaming from the other user:agoraEngine.on("stream-added")agoraEngine.subscribeagoraEngine.on("stream-subscribed")Receive data streamLeave live streaming eventagoraEngine.leave \ No newline at end of file diff --git a/assets/images/video-sdk/integrated-token-generation.svg b/assets/images/video-sdk/integrated-token-generation.svg index ba59d35ec..674b682f6 100644 --- a/assets/images/video-sdk/integrated-token-generation.svg +++ b/assets/images/video-sdk/integrated-token-generation.svg @@ -1,448 +1 @@ -Implemented by youProvided byAgoraUserUserAppAppDeveloper'sAuthenticationSystemDeveloper'sAuthenticationSystemSD-RTNSD-RTNJoin a Channel with AuthenticationStart the appLogin to youridentity management system.Select a channelRequest an Agora authentication token usingchannel name, role, token type and user IdValidate user requestagainst internal securityUse integrated Agora libraryto generate a tokenReturn the token to the clientJoin a channel with user Id, channel name, and tokenValidatethe tokenTrigger the callback after adding user to the channel \ No newline at end of file +Implemented by youProvided byAgoraUserUserAppAppDeveloper'sAuthenticationSystemDeveloper'sAuthenticationSystemSD-RTNSD-RTNJoin a Channel with AuthenticationStart the appLogin to youridentity management system.Select a channelRequest an Agora authentication token usingchannel name, role, token type and user IdValidate user requestagainst internal securityUse integrated Agora libraryto generate a tokenReturn the token to the clientJoin a channel with user Id, channel name, and tokenValidatethe tokenTrigger the callback after adding user to the channel \ No newline at end of file diff --git a/assets/images/video-sdk/media-stream-encryption.svg b/assets/images/video-sdk/media-stream-encryption.svg index 6879ac748..36a71688f 100644 --- a/assets/images/video-sdk/media-stream-encryption.svg +++ b/assets/images/video-sdk/media-stream-encryption.svg @@ -1,456 +1 @@ -Implemented by youProvided by AgoraUserUserAuthentication systemAuthentication systemAppAppSD-RTNSD-RTNStart the appInitiate the Video SDK engineSetup media stream encryptionLogin to theauthentication systemRetrieve a 32-byte keyRetrieve a 32-byte salt inBase64 formatCreate a encryption configuration usingthe key and saltSet the encryption configurationSelect a channel to joinRetrieve an access token.Join a channelCommunicate over anencrypted media stream \ No newline at end of file +Implemented by youProvided by AgoraUserUserAuthentication systemAuthentication systemAppAppSD-RTNSD-RTNStart the appInitiate the Video SDK engineSetup media stream encryptionLogin to theauthentication systemRetrieve a 32-byte keyRetrieve a 32-byte salt inBase64 formatCreate a encryption configuration usingthe key and saltSet the encryption configurationSelect a channel to joinRetrieve an access token.Join a channelCommunicate over anencrypted media stream \ No newline at end of file diff --git a/assets/images/video-sdk/play-drm-music.svg b/assets/images/video-sdk/play-drm-music.svg index e3e54f928..7f763970a 100644 --- a/assets/images/video-sdk/play-drm-music.svg +++ b/assets/images/video-sdk/play-drm-music.svg @@ -1,462 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNSet up the music content centerLoad the DRM extension framework.Create an instance of the music content center.Initialize the music content center.Find music from the content centerFind music.Call the method to search for music.Receive search results through the callback.Call the method to download music charts.Receive music charts data through the callback.Play DRM musicPress playPreload selected music.Create an instance of music player.Open and play music files. \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNSet up the music content centerLoad the DRM extension framework.Create an instance of the music content center.Initialize the music content center.Find music from the content centerFind music.Call the method to search for music.Receive search results through the callback.Call the method to download music charts.Receive music charts data through the callback.Play DRM musicPress playPreload selected music.Create an instance of music player.Open and play music files. \ No newline at end of file diff --git a/assets/images/video-sdk/product-workflow-web.svg b/assets/images/video-sdk/product-workflow-web.svg index d48c32f92..feecc4649 100644 --- a/assets/images/video-sdk/product-workflow-web.svg +++ b/assets/images/video-sdk/product-workflow-web.svg @@ -1,482 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppUse Video SDK to create an Agora Engine instanceCreate and play the local audio/video tracksBypass autoplay block whenonAutoplayFailed event occursJoinJoin a channelRetrieve authentication token to join a channelJoin the channelPublish and SubscribePublish camera and microphone tracks to the channelSubscribe to tracks from other usersManage local and remote audio/video tracksCommon workflowsBypass autoplay blockingStart screen sharingCreate a screen trackUnpublish the local video trackPublish the screen trackAdjust volumeCall API methods to adjust or mutethe local or remote audio trackMute/Unmute videoCall the API method to mute or unmutethe local video trackLeave the channelLeave the channel \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppUse Video SDK to create an Agora Engine instanceCreate and play the local audio/video tracksBypass autoplay block whenonAutoplayFailed event occursJoinJoin a channelRetrieve authentication token to join a channelJoin the channelPublish and SubscribePublish camera and microphone tracks to the channelSubscribe to tracks from other usersManage local and remote audio/video tracksCommon workflowsBypass autoplay blockingStart screen sharingCreate a screen trackUnpublish the local video trackPublish the screen trackAdjust volumeCall API methods to adjust or mutethe local or remote audio trackMute/Unmute videoCall the API method to mute or unmutethe local video trackLeave the channelLeave the channel \ No newline at end of file diff --git a/assets/images/video-sdk/product-workflow.svg b/assets/images/video-sdk/product-workflow.svg new file mode 100644 index 000000000..865d963b3 --- /dev/null +++ b/assets/images/video-sdk/product-workflow.svg @@ -0,0 +1 @@ +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppUse Video SDK to create an Agora Engine instanceEnable audio and video in the engineJoinJoin a channelRetrieve authentication token to join a channelJoin the channelPublish and SubscribePublish camera and microphone streams to the channelSubscribe to streams from other usersManage local and remote streamsCommon workflowsStart screen sharingCapture and publish your screen to the channelAdjust volumeCall API methods to adjust or mute volumeLeave the channelLeave the channel \ No newline at end of file diff --git a/assets/images/video-sdk/real-time-transcription-server.puml b/assets/images/video-sdk/real-time-transcription-server.puml deleted file mode 100644 index f7ca11181..000000000 --- a/assets/images/video-sdk/real-time-transcription-server.puml +++ /dev/null @@ -1,51 +0,0 @@ -@startuml -!include agora_skin.iuml - -box "Implemented by you" -participant "Your App" as APP -participant "Business server" as API -end box - -box "Agora" -participant "Real-Time Transcription" as RTT -end box - -group Start a task -APP -> API: Start real-time transcription \n -API -> API: Check user privileges and payment options (optional) -API -> RTT: Send an acquire request to fetch a builderToken -API <- RTT: Receive a builderToken in response -API -> API: Store builderToken against appID and channel name - -API -> API: Create JSON configuration for starting \nreal-time transcription -API -> RTT: Send a start request containing the \nbuilderToken and JSON configuration -API <- RTT: Receive the status and taskId in response -API -> API: Store taskId against appID and channel name -APP <- API: Success -end - -group Display subtitles -RTT -> APP: Send text data -APP -> APP: Receive data from -APP -> APP: Parse subtitle data and display text -end - -group Query task status -APP -> API: Check transcription task status \n -API -> API: Retrieve taskId and builderToken -API -> RTT: Send a query request containing \nthe taskId and builderToken -API <- RTT: Receive the current task status in response -APP <- API: Send the status to the client -end - -group Stop the task -APP -> API: Stop real-time transcription -API -> API: Retrieve taskId and builderToken -API -> RTT: Send a stop request containing \nthe taskId and builderToken -API <- RTT: Receive a response confirming success -API -> API: Remove builderToken of -API -> API: Remove taskId of -APP <- API: Confirm success -end - -@enduml diff --git a/assets/images/video-sdk/real-time-transcription-server.svg b/assets/images/video-sdk/real-time-transcription-server.svg deleted file mode 100644 index 58df3e56a..000000000 --- a/assets/images/video-sdk/real-time-transcription-server.svg +++ /dev/null @@ -1 +0,0 @@ -Implemented by youAgoraYour AppYour AppBusiness serverBusiness serverReal-Time TranscriptionReal-Time TranscriptionStart a taskStart real-time transcription<channel name>Check user privileges and payment options (optional)Send an acquire request to fetch a builderTokenReceive a builderToken in responseStore builderToken against appID and channel nameCreate JSON configuration for startingreal-time transcriptionSend a start request containing thebuilderToken and JSON configurationReceive the status and taskId in responseStore taskId against appID and channel nameSuccess <subtitle data uid>Display subtitlesSend text dataReceive data from <subtitle data uid>Parse subtitle data and display textQuery task statusCheck transcription task status<channel name>Retrieve taskId and builderTokenSend a query request containingthe taskId and builderTokenReceive the current task status in responseSend the status to the clientStop the taskStop real-time transcriptionRetrieve taskId and builderTokenSend a stop request containingthe taskId and builderTokenReceive a response confirming successRemove builderToken of <channel name>Remove taskId of <channel name>Confirm success \ No newline at end of file diff --git a/assets/images/video-sdk/spatial-audio-web.svg b/assets/images/video-sdk/spatial-audio-web.svg index c41e50796..a7a700ea4 100644 --- a/assets/images/video-sdk/spatial-audio-web.svg +++ b/assets/images/video-sdk/spatial-audio-web.svg @@ -1 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNSpatial AudioExtensionSpatial AudioExtensionAgoraRTC.createClientSetup the spatial audio extensionAgoraRTC.registerExtensionspatialAudioExtension.updateSelfPositionJoin ChannelAgoraRTCClient.joinRealize remote user's spatial soundclient.on("user-published")AgoraRTCClient.subscribespatialAudioExtension.createProcessorremoteTrack.pipe(processor).pipe(track.processorDestination)remoteTrack.Playprocessor.updateRemotePositionSpatial audio effect for media playerPlay media fileAgoraRTC.createBufferSourceAudioTrackspatialAudioExtension.createProcessortrack.pipe(processor).pipe(track.processorDestination)track.Playprocessor.updatePlayerPositionInfoCleanupclient.on("user-unpublished")processor.removeRemotePositionAgoraRTC.leaveprocessor.clearRemotePosition \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNSpatial AudioExtensionSpatial AudioExtensionAgoraRTC.createClientSetup the spatial audio extensionAgoraRTC.registerExtensionspatialAudioExtension.updateSelfPositionJoin ChannelAgoraRTCClient.joinRealize remote user's spatial soundclient.on("user-published")AgoraRTCClient.subscribespatialAudioExtension.createProcessorremoteTrack.pipe(processor).pipe(track.processorDestination)remoteTrack.Playprocessor.updateRemotePositionSpatial audio effect for media playerPlay media fileAgoraRTC.createBufferSourceAudioTrackspatialAudioExtension.createProcessortrack.pipe(processor).pipe(track.processorDestination)track.Playprocessor.updatePlayerPositionInfoCleanupclient.on("user-unpublished")processor.removeRemotePositionAgoraRTC.leaveprocessor.clearRemotePosition \ No newline at end of file diff --git a/assets/images/video-sdk/spatial-audio.svg b/assets/images/video-sdk/spatial-audio.svg index ed780fad6..cb247ab90 100644 --- a/assets/images/video-sdk/spatial-audio.svg +++ b/assets/images/video-sdk/spatial-audio.svg @@ -1,478 +1 @@ -Your client and serverAgoraUserUserAgora SDKAgora SDKYour ServerYour ServerSD-RTNSD-RTNLocal SpatialAudio EngineLocal SpatialAudio EngineEnable spatial audioInitialize Local Spatial Audio EngineCreate local spatial audio engineInitialize the engineSpatial audio effects for usersSend local spatial positionReceive spatial position of remote user(s)Call update self positionCall update remote positionSpatial audio effects for media playerSet spatial position of the userSet spatial position of the media playerSend and receive spatial audioClean upClear remote positionsDestroy the spatial engine \ No newline at end of file +Your client and serverAgoraUserUserAgora SDKAgora SDKYour ServerYour ServerSD-RTNSD-RTNLocal SpatialAudio EngineLocal SpatialAudio EngineEnable spatial audioInitialize Local Spatial Audio EngineCreate local spatial audio engineInitialize the engineSpatial audio effects for usersSend local spatial positionReceive spatial position of remote user(s)Call update self positionCall update remote positionSpatial audio effects for media playerSet spatial position of the userSet spatial position of the media playerSend and receive spatial audioClean upClear remote positionsDestroy the spatial engine \ No newline at end of file diff --git a/assets/images/video-sdk/video-call-logic-android.puml b/assets/images/video-sdk/video-call-logic-android.puml index 8e1476c6a..db79a041e 100644 --- a/assets/images/video-sdk/video-call-logic-android.puml +++ b/assets/images/video-sdk/video-call-logic-android.puml @@ -27,8 +27,7 @@ APP -> APP: Start local preview:\n agoraEngine.startPreview() APP -> APP: Retrieve authentication token to join channel APP -> API: Join the channel:\n agoraEngine.joinChannel() APP <- API: Remote user joined:\n onUserJoined() -APP -> API: Retrieve streaming from the remote user: \n agoraEngine.setupRemoteVideo(VideoCanvas) -API <-> APP: Receive and send data streams +APP -> API: Display video from a remote user: \n agoraEngine.setupRemoteVideo(VideoCanvas) end USR -> APP: Leave the call diff --git a/assets/images/video-sdk/video-call-logic-android.svg b/assets/images/video-sdk/video-call-logic-android.svg index 24a916ae2..7ca169634 100644 --- a/assets/images/video-sdk/video-call-logic-android.svg +++ b/assets/images/video-sdk/video-call-logic-android.svg @@ -1 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Agora Video SDK engine:agoraEngine = RtcEngine.createEnable the video module:agoraEngine.enableVideo()UserJoin a callSetup local video:agoraEngine.setupLocalVideo(VideoCanvas)Start local preview:agoraEngine.startPreview()Retrieve authentication token to join channelJoin the channel:agoraEngine.joinChannel()Remote user joined:onUserJoined()Retrieve streaming from the remote user:agoraEngine.setupRemoteVideo(VideoCanvas)Receive and send data streamsLeave the callLeave the channel:agoraEngine.leaveChannel()Close appClean up local resources:agoraEngine.destroy() \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Agora Video SDK engine:agoraEngine = RtcEngine.createEnable the video module:agoraEngine.enableVideo()UserJoin a callSetup local video:agoraEngine.setupLocalVideo(VideoCanvas)Start local preview:agoraEngine.startPreview()Retrieve authentication token to join channelJoin the channel:agoraEngine.joinChannel()Remote user joined:onUserJoined()Display video from a remote user:agoraEngine.setupRemoteVideo(VideoCanvas)Leave the callLeave the channel:agoraEngine.leaveChannel()Close appClean up local resources:agoraEngine.destroy() diff --git a/assets/images/video-sdk/video-call-logic-electron.svg b/assets/images/video-sdk/video-call-logic-electron.svg index 6f190f53d..81faa4b28 100644 --- a/assets/images/video-sdk/video-call-logic-electron.svg +++ b/assets/images/video-sdk/video-call-logic-electron.svg @@ -1 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppCreate an instance of the Video SDK engine:agoraEngine = agoraEngine.createAgoraRtcEngineInitialize the created instance:agoraEngine.initializeSet the role as host:agoraEngine.setClientRole(ClientRoleType.ClientRoleBroadcaster)Setup the callback functions:agoraEngine.registerEventHandlerUserStart callRetrieve authentication token to join channelSetup local video: agoraEngine.setupLocalVideoEnable the local video capturer:agoraEngine.enableVideoStart local preview :agoraEngine.startPreviewJoin a channel:agoraEngine.joinChannelJoin acceptedRetrieve streaming from the other user:agoraEngine.setupRemoteVideoReceive and send data streamsLeave callStop the local preview:stopPreviewleave the channel:agoraEngine.leaveChannel \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppCreate an instance of the Video SDK engine:agoraEngine = agoraEngine.createAgoraRtcEngineInitialize the created instance:agoraEngine.initializeSet the role as host:agoraEngine.setClientRole(ClientRoleType.ClientRoleBroadcaster)Setup the callback functions:agoraEngine.registerEventHandlerUserStart callRetrieve authentication token to join channelSetup local video: agoraEngine.setupLocalVideoEnable the local video capturer:agoraEngine.enableVideoStart local preview :agoraEngine.startPreviewJoin a channel:agoraEngine.joinChannelJoin acceptedRetrieve streaming from the other user:agoraEngine.setupRemoteVideoReceive and send data streamsLeave callStop the local preview:stopPreviewleave the channel:agoraEngine.leaveChannel \ No newline at end of file diff --git a/assets/images/video-sdk/video-call-logic-flutter.svg b/assets/images/video-sdk/video-call-logic-flutter.svg index b8f1f139b..63a348e8f 100644 --- a/assets/images/video-sdk/video-call-logic-flutter.svg +++ b/assets/images/video-sdk/video-call-logic-flutter.svg @@ -1 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitialize the Agora Video SDK engine:agoraEngine = createAgoraRtcEngine()Enable the video module:agoraEngine.enableVideo()Register the event handler:agoraEngine.registerEventHandlerSetup AgoraVideoView widgetsfor local and remote videosVideo CallJoin a callSet a client role:agoraEngine.setClientRoleSet a channel profile:agoraEngine.setChannelProfileRetrieve authentication tokenJoin a channel using the token:agoraEngine.joinChannelStart local peview:agoraEngine.startPreview()Remote user joined:RtcEngineEventHandler onUserJoined:Send and receive data streamsDisplay remote video using AgoraVideoViewLeave callLeave the channelagoraEngine.leaveChannel()Close app \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitialize the Agora Video SDK engine:agoraEngine = createAgoraRtcEngine()Enable the video module:agoraEngine.enableVideo()Register the event handler:agoraEngine.registerEventHandlerSetup AgoraVideoView widgetsfor local and remote videosVideo CallJoin a callSet a client role:agoraEngine.setClientRoleSet a channel profile:agoraEngine.setChannelProfileRetrieve authentication tokenJoin a channel using the token:agoraEngine.joinChannelStart local peview:agoraEngine.startPreview()Remote user joined:RtcEngineEventHandler onUserJoined:Send and receive data streamsDisplay remote video using AgoraVideoViewLeave callLeave the channelagoraEngine.leaveChannel()Close app \ No newline at end of file diff --git a/assets/images/video-sdk/video-call-logic-ios.svg b/assets/images/video-sdk/video-call-logic-ios.svg index e47741b0a..939684c80 100644 --- a/assets/images/video-sdk/video-call-logic-ios.svg +++ b/assets/images/video-sdk/video-call-logic-ios.svg @@ -1 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitiate the Video SDK engine:agoraEngine = AgoraRtcEngineKit.sharedEngineSetup the local video stream:agoraEngine.enableVideo()HostStart a callIn a call, all users send to the channel:agoraEngine.setClientRole(.broadcaster)Start local video:agoraEngine.setupLocalVideo(videoCanvas)Join the channel:agoraEngine?.joinChannelRetrieve streaming from the other user:agoraEngine.setupRemoteVideo(videoCanvas)Receive and send data streamsLeave the callStop local video:agoraEngine.stopPreview()Leave the channel:agoraEngine.leaveChannel(nil)Close appClean up local resources:AgoraRtcEngineKit.destroy() \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitiate the Video SDK engine:agoraEngine = AgoraRtcEngineKit.sharedEngineSetup the local video stream:agoraEngine.enableVideo()HostStart a callIn a call, all users send to the channel:agoraEngine.setClientRole(.broadcaster)Start local video:agoraEngine.setupLocalVideo(videoCanvas)Join the channel:agoraEngine?.joinChannelRetrieve streaming from the other user:agoraEngine.setupRemoteVideo(videoCanvas)Receive and send data streamsLeave the callStop local video:agoraEngine.stopPreview()Leave the channel:agoraEngine.leaveChannel(nil)Close appClean up local resources:AgoraRtcEngineKit.destroy() \ No newline at end of file diff --git a/assets/images/video-sdk/video-call-logic-reactjs.puml b/assets/images/video-sdk/video-call-logic-reactjs.puml index 8757e6e33..d7b76ab99 100644 --- a/assets/images/video-sdk/video-call-logic-reactjs.puml +++ b/assets/images/video-sdk/video-call-logic-reactjs.puml @@ -1,25 +1,22 @@ -@startuml video-call-logic-web +@startuml !include agora_skin.iuml actor "User" as USR box "Your app" - participant "Video SDK" as APP - end box box "Agora" - participant "SD-RTN™" as API - end box USR -> APP: Open App APP -> APP: Setup app to handle local hardware and streaming. + group User USR -> APP: Start call -APP -> APP: Create the agoraEngine\nconst agoraEngine = useRTCClient(AgoraRTC.createClient +APP -> APP: Create the agoraEngine\nconst agoraEngine = useRTCClient(AgoraRTC.createClient) APP -> APP: Retrieve authentication token to join channel APP -> API: Join a channel:\n useJoin API -> APP : Join accepted @@ -28,8 +25,8 @@ APP -> API: Push local media tracks to the channel:\n usePublish([localMicropho API -> APP: Retrieve streaming from the other user: \n API <-> APP: Receive and send data streams end -USR -> APP: Leave call -APP -> API: leave the channel:\n \n useJoin +USR -> APP: Leave call +APP -> API: Leave the channel:\n useJoin @enduml diff --git a/assets/images/video-sdk/video-call-logic-reactjs.svg b/assets/images/video-sdk/video-call-logic-reactjs.svg index 89d111cde..e9407aa1d 100644 --- a/assets/images/video-sdk/video-call-logic-reactjs.svg +++ b/assets/images/video-sdk/video-call-logic-reactjs.svg @@ -1 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTN™SD-RTN™Open AppSetup app to handle local hardware and streaming.UserStart callCreate the agoraEngineconst agoraEngine = useRTCClient(AgoraRTC.createClientRetrieve authentication token to join channelJoin a channel:useJoinJoin acceptedCreate local media tracks :const { isLoading: isLoadingCam, localCameraTrack } = useLocalCameraTrack();const { isLoading: isLoadingMic, localMicrophoneTrack } = useLocalMicrophPush local media tracks to the channel:usePublish([localMicrophoneTrack, localCameraTrack]);Retrieve streaming from the other user:<RemoteUser user={remoteUser} playVideo={true} playAudio={true} />Receive and send data streamsLeave callleave the channel: useJoin \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNâ„¢SD-RTNâ„¢Open AppSetup app to handle local hardware and streaming.UserStart callCreate the agoraEngineconst agoraEngine = useRTCClient(AgoraRTC.createClient)Retrieve authentication token to join channelJoin a channel:useJoinJoin acceptedCreate local media tracks :const { isLoading: isLoadingCam, localCameraTrack } = useLocalCameraTrack();const { isLoading: isLoadingMic, localMicrophoneTrack } = useLocalMicrophPush local media tracks to the channel:usePublish([localMicrophoneTrack, localCameraTrack]);Retrieve streaming from the other user:<RemoteUser user={remoteUser} playVideo={true} playAudio={true} />Receive and send data streamsLeave callLeave the channel:useJoin diff --git a/assets/images/video-sdk/video-call-logic-template.svg b/assets/images/video-sdk/video-call-logic-template.svg index 571701706..163ea25d6 100644 --- a/assets/images/video-sdk/video-call-logic-template.svg +++ b/assets/images/video-sdk/video-call-logic-template.svg @@ -1,468 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitiate the Video SDK engine.Start video in the engine.HostStart callIn a call, all users broadcast to the channel.Start local video.Join the channel.Retrieve streaming from the other user.Receive and send data streamsLeave callStop local video.Leave the channel.Close appClean up local resources. \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen appInitiate the Video SDK engine.Start video in the engine.HostStart callIn a call, all users broadcast to the channel.Start local video.Join the channel.Retrieve streaming from the other user.Receive and send data streamsLeave callStop local video.Leave the channel.Close appClean up local resources. \ No newline at end of file diff --git a/assets/images/video-sdk/video-call-logic-unity.puml b/assets/images/video-sdk/video-call-logic-unity.puml index 57269209b..2ceda1875 100644 --- a/assets/images/video-sdk/video-call-logic-unity.puml +++ b/assets/images/video-sdk/video-call-logic-unity.puml @@ -25,7 +25,6 @@ USR -> APP: Start call APP -> APP: Enable the video module: \n RtcEngine.EnableVideo() APP -> API: Set the user role as broadcaster: \n RtcEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER) APP -> API: Join call: \n RtcEngine.JoinChannel() -APP <-> API: Receive and send data stream end USR -> APP: Leave call diff --git a/assets/images/video-sdk/video-call-logic-unity.svg b/assets/images/video-sdk/video-call-logic-unity.svg index cf9c1aa56..45bc4291b 100644 --- a/assets/images/video-sdk/video-call-logic-unity.svg +++ b/assets/images/video-sdk/video-call-logic-unity.svg @@ -1 +1 @@ -Your gameAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen gameCreate an RtcEngine instance:RtcEngine = Agora.Rtc.RtcEngine.CreateAgoraRtcEngine()Set the context:RtcEngineContext context = new RtcEngineContext(_appID, 0, true,CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING,AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT)Initialize RtcEngine:RtcEngine.Initialize(context)Video CallStart callEnable the video module:RtcEngine.EnableVideo()Set the user role as broadcaster:RtcEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER)Join call:RtcEngine.JoinChannel()Receive and send data streamLeave callLeave the channel:RtcEngine.LeaveChannel()Disable the video modules:RtcEngine.DisableVideo()Close gameClean up local resources:RtcEngine.Dispose() \ No newline at end of file +Your gameAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen gameCreate an RtcEngine instance:RtcEngine = Agora.Rtc.RtcEngine.CreateAgoraRtcEngine()Set the context:RtcEngineContext context = new RtcEngineContext(_appID, 0, true,CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING,AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT)Initialize RtcEngine:RtcEngine.Initialize(context)Video CallStart callEnable the video module:RtcEngine.EnableVideo()Set the user role as broadcaster:RtcEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER)Join call:RtcEngine.JoinChannel()Leave callLeave the channel:RtcEngine.LeaveChannel()Disable the video modules:RtcEngine.DisableVideo()Close gameClean up local resources:RtcEngine.Dispose() diff --git a/assets/images/video-sdk/video-call-logic-unreal.puml b/assets/images/video-sdk/video-call-logic-unreal.puml index 81dff1b6e..8b20e3141 100644 --- a/assets/images/video-sdk/video-call-logic-unreal.puml +++ b/assets/images/video-sdk/video-call-logic-unreal.puml @@ -36,4 +36,4 @@ APP -> API: Leave the channel: \n agoraEngine->leaveChannel() USR -> APP: Close app APP -> APP: Clean up local resources: \n agoraEngine->release() -@enduml +@enduml \ No newline at end of file diff --git a/assets/images/video-sdk/video-call-logic-unreal.svg b/assets/images/video-sdk/video-call-logic-unreal.svg index 8155e353f..6b670fd49 100644 --- a/assets/images/video-sdk/video-call-logic-unreal.svg +++ b/assets/images/video-sdk/video-call-logic-unreal.svg @@ -1 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Agora Video SDK engine:agoraEngine = agora::rtc::ue::createAgoraRtcEngine()Enable the audio and video modules:agoraEngine->enableVideo()agoraEngine->enableAudio();UserJoin a callSetup local video:agoraEngine->setupLocalVideo(videoCanvas)Retrieve authentication token to join channelJoin the channel:agoraEngine->joinChannel()Remote user joined:onUserJoined()Retrieve streaming from the remote user:agoraEngine->setupRemoteVideo(videoCanvas)Receive and send data streamsLeave the callLeave the channel:agoraEngine->leaveChannel()Close appClean up local resources:agoraEngine->release() +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Agora Video SDK engine:agoraEngine = agora::rtc::ue::createAgoraRtcEngine()Enable the audio and video modules:agoraEngine->enableVideo()agoraEngine->enableAudio();UserJoin a callSetup local video:agoraEngine->setupLocalVideo(videoCanvas)Retrieve authentication token to join channelJoin the channel:agoraEngine->joinChannel()Remote user joined:onUserJoined()Retrieve streaming from the remote user:agoraEngine->setupRemoteVideo(videoCanvas)Receive and send data streamsLeave the callLeave the channel:agoraEngine->leaveChannel()Close appClean up local resources:agoraEngine->release() \ No newline at end of file diff --git a/assets/images/video-sdk/video-call-logic-web.svg b/assets/images/video-sdk/video-call-logic-web.svg index b27d5d3c5..002a563f8 100644 --- a/assets/images/video-sdk/video-call-logic-web.svg +++ b/assets/images/video-sdk/video-call-logic-web.svg @@ -1 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppInitiate the Video SDK engine:agoraEngine = AgoraRTC.createClientSet the required event listners:agoraEngine.on("user-published")agoraEngine.on("user-unpublished")UserStart callRetrieve authentication token to join channelJoin a channel:agoraEngine.joinJoin acceptedCreate local media tracks :AgoraRTC.createMicrophoneAudioTrackAgoraRTC.createCameraVideoTrackPush local media tracks to the channel:agoraEngine.publishRetrieve streaming from the other user:agoraEngine.on("user-published")Play remote media tracks: remoteVideoTrack.playremoteAudioTrack.playReceive and send data streamsLeave callleave the channel:agoraEngine.leave \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNâ„¢SD-RTNâ„¢Open AppSetup app to handle local hardware and streaming.UserStart callCreate the agoraEngineconst agoraEngine = useRTCClient(AgoraRTC.createClientRetrieve authentication token to join channelJoin a channel:useJoinJoin acceptedCreate local media tracks :const { isLoading: isLoadingCam, localCameraTrack } = useLocalCameraTrack();const { isLoading: isLoadingMic, localMicrophoneTrack } = useLocalMicrophPush local media tracks to the channel:usePublish([localMicrophoneTrack, localCameraTrack]);Retrieve streaming from the other user:<RemoteUser user={remoteUser} playVideo={true} playAudio={true} />Receive and send data streamsLeave callleave the channel: useJoin \ No newline at end of file diff --git a/assets/images/video-sdk/video_call_workflow.svg b/assets/images/video-sdk/video_call_workflow.svg new file mode 100644 index 000000000..83aa472a0 --- /dev/null +++ b/assets/images/video-sdk/video_call_workflow.svg @@ -0,0 +1 @@ +Call implementerAgoraClientClientToken ServerToken ServerClientClientAgora PlatformAgora PlatformSetup clientUser login to implementor securityAuthenticate and retrieve tokenUser login to implementor securityAuthenticate and retrieve tokenCreate client instanceSet client role: hostCreate client instanceSet client role: audienceRun callConnect to local audio and video resourcesJoin channelSend audio and video to channelJoin channelSend audio and video to channelCommunicateEnd callClose audio and videoLeave callClose audio and videoLeave call \ No newline at end of file diff --git a/assets/images/video-sdk/video_call_workflow_run_end.svg b/assets/images/video-sdk/video_call_workflow_run_end.svg new file mode 100644 index 000000000..ae2b2a5a8 --- /dev/null +++ b/assets/images/video-sdk/video_call_workflow_run_end.svg @@ -0,0 +1 @@ +Call implementerAgoraClientClientClientClientAgora PlatformAgora PlatformRun callConnect to local audio and video resourcesJoin channelSend audio and video to channelConnect to local audio and video resourcesJoin channelSend audio and video to channelCommunicateEnd callClose audio and videoLeave channelClose audio and videoLeave channel \ No newline at end of file diff --git a/assets/images/voice-sdk/authentication-logic.svg b/assets/images/voice-sdk/authentication-logic.svg index 7e964a030..71c783045 100644 --- a/assets/images/voice-sdk/authentication-logic.svg +++ b/assets/images/voice-sdk/authentication-logic.svg @@ -1,450 +1 @@ -Implemented by youProvided by AgoraToken serverToken serverAppAppSD-RTNSD-RTNJoin a channel with authenticationRequest a token using channel name,role, token type, and user IDValidate user againstinternal securityGenerate a token and return it to the clientJoin a channel with uid,channel name, and tokenValidatethe tokenTrigger the callback afteradding user to the channelRenew tokenTrigger event: Token Privilege will ExpireRequest a fresh token using channel name,role, token type, and user IDValidate user request against internal logicGenerate a fresh token and return it to the clientSend the fresh tokenwith a RenewToken request \ No newline at end of file +Implemented by youProvided by AgoraToken serverToken serverAppAppSD-RTNSD-RTNJoin a channel with authenticationRequest a token using channel name,role, token type, and user IDValidate user againstinternal securityGenerate a token and return it to the clientJoin a channel with uid,channel name, and tokenValidatethe tokenTrigger the callback afteradding user to the channelRenew tokenTrigger event: Token Privilege will ExpireRequest a fresh token using channel name,role, token type, and user IDValidate user request against internal logicGenerate a fresh token and return it to the clientSend the fresh tokenwith a RenewToken request \ No newline at end of file diff --git a/assets/images/voice-sdk/ensure-voice-quality.svg b/assets/images/voice-sdk/ensure-voice-quality.svg index 61f224ae5..ccd3465a3 100644 --- a/assets/images/voice-sdk/ensure-voice-quality.svg +++ b/assets/images/voice-sdk/ensure-voice-quality.svg @@ -1,494 +1 @@ -Your appAgoraUserUserVoice SDKVoice SDKSD-RTNSD-RTNOpen AppSet log file configuration and create engineSet log file parametersUse Voice SK to create an instance of Agora EnginePre-call testsStart the echo testCall the method to start the echo testSend and receive backaudio after a delayto test hardware and network qualityStart the network probe testCall the method to start the network probe testDeliver network quality scoreand network statisticsSet the audio profileSpecify audio profile and scenariobased on the nature of the appCall the method to setthe audio profile and scenarioJoin channelJoin channelMonitor in-call qualityEnable the quality statisticsRecieve network, call, and audio quality statisticsRecieve state change notificationsNotify the userTake corrective actionEcho cancellationChoose the audio file to be playedand specify mixing optionsCall the method tostart audio mixingUses echo-cancellationfeatures to remove echo \ No newline at end of file +Your appAgoraUserUserVoice SDKVoice SDKSD-RTNSD-RTNOpen AppSet log file configuration and create engineSet log file parametersUse Voice SK to create an instance of Agora EnginePre-call testsStart the echo testCall the method to start the echo testSend and receive backaudio after a delayto test hardware and network qualityStart the network probe testCall the method to start the network probe testDeliver network quality scoreand network statisticsSet the audio profileSpecify audio profile and scenariobased on the nature of the appCall the method to setthe audio profile and scenarioJoin channelJoin channelMonitor in-call qualityEnable the quality statisticsRecieve network, call, and audio quality statisticsRecieve state change notificationsNotify the userTake corrective actionEcho cancellationChoose the audio file to be playedand specify mixing optionsCall the method tostart audio mixingUses echo-cancellationfeatures to remove echo \ No newline at end of file diff --git a/assets/images/voice-sdk/geofencing.svg b/assets/images/voice-sdk/geofencing.svg index bf480faab..1ded373b7 100644 --- a/assets/images/voice-sdk/geofencing.svg +++ b/assets/images/voice-sdk/geofencing.svg @@ -1,444 +1 @@ -Implemented by youProvided by AgoraUserUserAppAppSD-RTNSD-RTNStart the appGeofencingSet SD-RTN region in the Agoraengine configurationInitiate the Agora engineConnect to SD-RTN in aspecific regionSuccess responseSelect a channel to joinJoin a channel with userId, channel name, and tokenJoin accepted \ No newline at end of file +Implemented by youProvided by AgoraUserUserAppAppSD-RTNSD-RTNStart the appGeofencingSet SD-RTN region in the Agoraengine configurationInitiate the Agora engineConnect to SD-RTN in aspecific regionSuccess responseSelect a channel to joinJoin a channel with userId, channel name, and tokenJoin accepted \ No newline at end of file diff --git a/assets/images/voice-sdk/integrated-token-generation.svg b/assets/images/voice-sdk/integrated-token-generation.svg index ba59d35ec..674b682f6 100644 --- a/assets/images/voice-sdk/integrated-token-generation.svg +++ b/assets/images/voice-sdk/integrated-token-generation.svg @@ -1,448 +1 @@ -Implemented by youProvided byAgoraUserUserAppAppDeveloper'sAuthenticationSystemDeveloper'sAuthenticationSystemSD-RTNSD-RTNJoin a Channel with AuthenticationStart the appLogin to youridentity management system.Select a channelRequest an Agora authentication token usingchannel name, role, token type and user IdValidate user requestagainst internal securityUse integrated Agora libraryto generate a tokenReturn the token to the clientJoin a channel with user Id, channel name, and tokenValidatethe tokenTrigger the callback after adding user to the channel \ No newline at end of file +Implemented by youProvided byAgoraUserUserAppAppDeveloper'sAuthenticationSystemDeveloper'sAuthenticationSystemSD-RTNSD-RTNJoin a Channel with AuthenticationStart the appLogin to youridentity management system.Select a channelRequest an Agora authentication token usingchannel name, role, token type and user IdValidate user requestagainst internal securityUse integrated Agora libraryto generate a tokenReturn the token to the clientJoin a channel with user Id, channel name, and tokenValidatethe tokenTrigger the callback after adding user to the channel \ No newline at end of file diff --git a/assets/images/voice-sdk/process-raw-audio.svg b/assets/images/voice-sdk/process-raw-audio.svg new file mode 100644 index 000000000..c86fe78cf --- /dev/null +++ b/assets/images/voice-sdk/process-raw-audio.svg @@ -0,0 +1 @@ +Your appAgoraUserUserVoice SDKVoice SDKSD-RTNSD-RTNOpen AppCreate an instance of Agora Engine using Voice SDKSetup raw data processingSetup the audio frame observerJoinJoin a channelRegister the audio frame observerSet audio frame parametersRetrieve authentication token to join a channelJoin the channelProcess raw audio dataGet the raw data in the callbacksSend the processed data back with the callbacksLeaveLeave the channelUnegister the audio frame observerLeave the channel \ No newline at end of file diff --git a/assets/images/voice-sdk/product-workflow-voice-web.svg b/assets/images/voice-sdk/product-workflow-voice-web.svg index 96435defa..de3db9e7d 100644 --- a/assets/images/voice-sdk/product-workflow-voice-web.svg +++ b/assets/images/voice-sdk/product-workflow-voice-web.svg @@ -1,476 +1 @@ -Your appAgoraUserUserVoice SDKVoice SDKSD-RTNSD-RTNOpen AppUse Voice SDK to create an Agora Engine instanceCreate and play the local audio trackBypass autoplay block whenonAutoplayFailed event occursJoinJoin a channelRetrieve authentication token to join a channelJoin the channelPublish and SubscribePublish the microphone track to the channelSubscribe to tracks from other usersManage local and remote audio tracksCommon workflowsBypass autoplay blockingUnpublish the local audio trackAdjust volumeCall API methods to adjust or mutethe local or remote audio trackMute/Unmute audioCall the API method to mute or unmutethe local audio trackLeave the channelLeave the channel \ No newline at end of file +Your appAgoraUserUserVoice SDKVoice SDKSD-RTNSD-RTNOpen AppUse Voice SDK to create an Agora Engine instanceCreate and play the local audio trackBypass autoplay block whenonAutoplayFailed event occursJoinJoin a channelRetrieve authentication token to join a channelJoin the channelPublish and SubscribePublish the microphone track to the channelSubscribe to tracks from other usersManage local and remote audio tracksCommon workflowsBypass autoplay blockingUnpublish the local audio trackAdjust volumeCall API methods to adjust or mutethe local or remote audio trackMute/Unmute audioCall the API method to mute or unmutethe local audio trackLeave the channelLeave the channel \ No newline at end of file diff --git a/assets/images/voice-sdk/product-workflow-voice.svg b/assets/images/voice-sdk/product-workflow-voice.svg new file mode 100644 index 000000000..992da5a98 --- /dev/null +++ b/assets/images/voice-sdk/product-workflow-voice.svg @@ -0,0 +1 @@ +Your appAgoraUserUserVoice SDKVoice SDKSD-RTNSD-RTNOpen AppUse Voice SDK to create an Agora Engine instanceJoinJoin a channelRetrieve authentication token to join a channelJoin the channelPublish and SubscribePublish microphone stream to the channelSubscribe to streams from other usersManage local and remote streamsCommon workflowsAdjust volumeCall API methods to adjust or mute volumeLeave the channelLeave the channel \ No newline at end of file diff --git a/assets/images/voice-sdk/voice-call-logic-android.puml b/assets/images/voice-sdk/voice-call-logic-android.puml new file mode 100644 index 000000000..a0506cf88 --- /dev/null +++ b/assets/images/voice-sdk/voice-call-logic-android.puml @@ -0,0 +1,33 @@ +@startuml + +!include agora_skin.iuml + +actor "User" as USR + +box "Your app" +participant "Voice SDK" as APP +end box + +box "Agora" +participant "SD-RTN" as API +end box + +USR -> APP: Open App +APP -> APP: Initiate the Agora Voice SDK engine: \n agoraEngine = RtcEngine.create + +group User +USR -> APP: Join a call +APP -> APP: Retrieve authentication token to join channel +APP -> API: Join the channel:\n agoraEngine.joinChannel() +APP <- API: Remote user joined:\n onUserJoined() +end + +USR -> APP: Leave the call +APP -> API: Leave the channel: \n agoraEngine.leaveChannel() + +USR -> APP: Close app +APP -> APP: Clean up local resources: \n agoraEngine.destroy() + + +@enduml + diff --git a/assets/images/voice-sdk/voice-call-logic-android.svg b/assets/images/voice-sdk/voice-call-logic-android.svg index 754e5d7b1..bd7f1618a 100644 --- a/assets/images/voice-sdk/voice-call-logic-android.svg +++ b/assets/images/voice-sdk/voice-call-logic-android.svg @@ -1,462 +1 @@ -Your appAgoraUserUserVoice SDKVoice SDKSD-RTNSD-RTNOpen AppInitiate the Agora Voice SDK engine:agoraEngine = RtcEngine.createUserJoin a callRetrieve authentication token to join channelJoin the channel:agoraEngine.joinChannel()Remote user joined:onUserJoined()Receive and send data streamsLeave the callLeave the channel:agoraEngine.leaveChannel()Close appClean up local resources:agoraEngine.destroy() \ No newline at end of file +Your appAgoraUserUserVoice SDKVoice SDKSD-RTNSD-RTNOpen AppInitiate the Agora Voice SDK engine:agoraEngine = RtcEngine.createUserJoin a callRetrieve authentication token to join channelJoin the channel:agoraEngine.joinChannel()Remote user joined:onUserJoined()Leave the callLeave the channel:agoraEngine.leaveChannel()Close appClean up local resources:agoraEngine.destroy() \ No newline at end of file diff --git a/assets/images/voice-sdk/voice-call-logic-electron.svg b/assets/images/voice-sdk/voice-call-logic-electron.svg index bc6d279cd..437257c0f 100644 --- a/assets/images/voice-sdk/voice-call-logic-electron.svg +++ b/assets/images/voice-sdk/voice-call-logic-electron.svg @@ -1,454 +1 @@ -Your appAgoraUserUserVoice SDKVoice SDKSD-RTNSD-RTNOpen AppCreate an instance of the Voice SDK engine:agoraEngine = agoraEngine.createAgoraRtcEngineInitialize the created instance:agoraEngine.initializeUserStart callRetrieve authentication token to join channelJoin a channel:agoraEngine.joinChannelJoin acceptedReceive and send audio streamLeave callleave the channel:agoraEngine.leaveChannel \ No newline at end of file +Your appAgoraUserUserVoice SDKVoice SDKSD-RTNSD-RTNOpen AppCreate an instance of the Voice SDK engine:agoraEngine = agoraEngine.createAgoraRtcEngineInitialize the created instance:agoraEngine.initializeUserStart callRetrieve authentication token to join channelJoin a channel:agoraEngine.joinChannelJoin acceptedReceive and send audio streamLeave callleave the channel:agoraEngine.leaveChannel \ No newline at end of file diff --git a/assets/images/voice-sdk/voice-call-logic-flutter.svg b/assets/images/voice-sdk/voice-call-logic-flutter.svg index 72bfacb02..e8ba605b8 100644 --- a/assets/images/voice-sdk/voice-call-logic-flutter.svg +++ b/assets/images/voice-sdk/voice-call-logic-flutter.svg @@ -1,454 +1 @@ -Your appAgoraUserUserVoice SDKVoice SDKSD-RTNSD-RTNOpen appInitialize the Agora Voice SDK engine:agoraEngine = createAgoraRtcEngine()Register the event handler:agoraEngine.registerEventHandlerVoice CallJoin a callSet a client role:agoraEngine.setClientRoleSet a channel profile:agoraEngine.setChannelProfileRetrieve authentication tokenJoin a channel using the token:agoraEngine.joinChannelRemote user joined:RtcEngineEventHandler onUserJoined:Send and receive data streamsLeave callLeave the channelagoraEngine.leaveChannel()Close app \ No newline at end of file +Your appAgoraUserUserVoice SDKVoice SDKSD-RTNSD-RTNOpen appInitialize the Agora Voice SDK engine:agoraEngine = createAgoraRtcEngine()Register the event handler:agoraEngine.registerEventHandlerVoice CallJoin a callSet a client role:agoraEngine.setClientRoleSet a channel profile:agoraEngine.setChannelProfileRetrieve authentication tokenJoin a channel using the token:agoraEngine.joinChannelRemote user joined:RtcEngineEventHandler onUserJoined:Send and receive data streamsLeave callLeave the channelagoraEngine.leaveChannel()Close app \ No newline at end of file diff --git a/assets/images/voice-sdk/voice-call-logic-unity.puml b/assets/images/voice-sdk/voice-call-logic-unity.puml index f0e5bb858..d69415bf5 100644 --- a/assets/images/voice-sdk/voice-call-logic-unity.puml +++ b/assets/images/voice-sdk/voice-call-logic-unity.puml @@ -17,9 +17,7 @@ end box USR -> APP: Open app APP -> APP: Create an Agora Voice SDK engine instance: \n RtcEngine = Agora.Rtc.RtcEngine.CreateAgoraRtcEngine() -APP -> API: Set the context: \n RtcEngineContext context = new RtcEngineContext(_appID, 0, true, - CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING, - AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT) +APP -> API: Set the context: \n RtcEngineContext context = new RtcEngineContext(_appID, 0, true, \n CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING, \n AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT) APP -> APP: Initialize RtcEngine: \n RtcEngine.Initialize(context) group Audio Call @@ -27,7 +25,6 @@ USR -> APP: Start call APP -> APP: Enable the audio module: \n RtcEngine.EnableAudio() APP -> API: Set the user role as broadcaster: \n RtcEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER) APP -> API: Join the channel: \n RtcEngine.JoinChannel() -API <-> APP: Receive and send data stream end USR -> APP: Leave the call diff --git a/assets/images/voice-sdk/voice-call-logic-unity.svg b/assets/images/voice-sdk/voice-call-logic-unity.svg index 4a36623c8..c3fb1eb49 100644 --- a/assets/images/voice-sdk/voice-call-logic-unity.svg +++ b/assets/images/voice-sdk/voice-call-logic-unity.svg @@ -1,470 +1 @@ -Your appAgoraUserUserVoice SDKVoice SDKSD-RTNSD-RTNOpen AppInitiate the Agora Voice SDK engine:agoraEngine = RtcEngine.createUserJoin a callRetrieve authentication token to join channelJoin the channel:agoraEngine.joinChannel()Remote user joined:onUserJoined()Receive and send data streamsLeave the callLeave the channel:agoraEngine.leaveChannel()Close appClean up local resources:agoraEngine.destroy() \ No newline at end of file +Your appAgoraUserUserVoice SDKVoice SDKSD-RTNSD-RTNOpen appCreate an Agora Voice SDK engine instance:RtcEngine = Agora.Rtc.RtcEngine.CreateAgoraRtcEngine()Set the context:RtcEngineContext context = new RtcEngineContext(_appID, 0, true,CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING,AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT)Initialize RtcEngine:RtcEngine.Initialize(context)Audio CallStart callEnable the audio module:RtcEngine.EnableAudio()Set the user role as broadcaster:RtcEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER)Join the channel:RtcEngine.JoinChannel()Leave the callLeave the channel:RtcEngine.LeaveChannel()Close appDisable the audio modules:RtcEngine.DisableAudio() diff --git a/broadcast-streaming/develop/media-stream-encryption.mdx b/broadcast-streaming/develop/media-stream-encryption.mdx index cf526c8c5..e832dd8d4 100644 --- a/broadcast-streaming/develop/media-stream-encryption.mdx +++ b/broadcast-streaming/develop/media-stream-encryption.mdx @@ -1,5 +1,5 @@ --- -title: 'Secure channels with encryption' +title: 'Secure channel encryption' sidebar_position: 4 type: docs description: > diff --git a/broadcast-streaming/enable-features/image-enhancement.mdx b/broadcast-streaming/enable-features/image-enhancement.mdx deleted file mode 100644 index 2f1e79e04..000000000 --- a/broadcast-streaming/enable-features/image-enhancement.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: 'Image enhancer (beta)' -sidebar_position: 17 -type: docs -description: > - Gives you granular control over the degree of image enhancement, such as skin lightening, skin smoothing, and red saturation. ---- - -import ImageEnhancer from '@docs/shared/extensions-marketplace/image-enhancement.mdx'; - -export const toc = [{}]; - - \ No newline at end of file diff --git a/broadcast-streaming/reference/pricing.mdx b/broadcast-streaming/overview/pricing.mdx similarity index 94% rename from broadcast-streaming/reference/pricing.mdx rename to broadcast-streaming/overview/pricing.mdx index 02231738e..a7fe219e5 100644 --- a/broadcast-streaming/reference/pricing.mdx +++ b/broadcast-streaming/overview/pricing.mdx @@ -1,6 +1,6 @@ --- title: Pricing -sidebar_position: 1 +sidebar_position: 3 description: > Provides you with information on billing, fee deductions, free-of-charge policy, and any suspension to your account based on the account type. --- diff --git a/broadcast-streaming/reference/release-notes.mdx b/broadcast-streaming/overview/release-notes.mdx similarity index 94% rename from broadcast-streaming/reference/release-notes.mdx rename to broadcast-streaming/overview/release-notes.mdx index c346b7709..b38c29e4a 100644 --- a/broadcast-streaming/reference/release-notes.mdx +++ b/broadcast-streaming/overview/release-notes.mdx @@ -1,6 +1,6 @@ --- title: 'Release notes' -sidebar_position: 2 +sidebar_position: 4 type: docs description: > Information about changes in each release of Video Calling. diff --git a/broadcast-streaming/reference/supported-platforms.mdx b/broadcast-streaming/overview/supported-platforms.mdx similarity index 100% rename from broadcast-streaming/reference/supported-platforms.mdx rename to broadcast-streaming/overview/supported-platforms.mdx diff --git a/broadcast-streaming/reference/error-codes.mdx b/broadcast-streaming/reference/error-codes.mdx new file mode 100644 index 000000000..5a43fd283 --- /dev/null +++ b/broadcast-streaming/reference/error-codes.mdx @@ -0,0 +1,13 @@ +--- +title: 'Error codes' +sidebar_position: 5 +type: docs +description: > + List of commonly encountered API errors and their causes. +--- + +import ErrorCodes from '@docs/shared/video-sdk/reference/_error-codes.mdx'; + +export const toc = [{}]; + + \ No newline at end of file diff --git a/broadcast-streaming/reference/known-issues.mdx b/broadcast-streaming/reference/known-issues.mdx deleted file mode 100644 index 8fbe59683..000000000 --- a/broadcast-streaming/reference/known-issues.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: 'Known issues' -sidebar_position: 2 -type: docs -description: > - Known issues and limitations of using the Web SDK. ---- - - -import KnownIssues from '@docs/shared/video-sdk/reference/_known-issues.mdx'; - -export const toc = [{}]; - - - diff --git a/broadcast-streaming/reference/service-limits.mdx b/broadcast-streaming/reference/service-limits.mdx new file mode 100644 index 000000000..d8199b945 --- /dev/null +++ b/broadcast-streaming/reference/service-limits.mdx @@ -0,0 +1,13 @@ +--- +title: 'Service limits' +sidebar_position: 5 +type: docs +description: > + The service limits imposed by Agora +--- + +import ServiceLimits from '@docs/shared/video-sdk/reference/_service_limits.mdx'; + +export const toc = [{}]; + + \ No newline at end of file diff --git a/cloud-recording/develop/individual-mode.md b/cloud-recording/develop/individual-mode.md index 617233f08..b3f3794d6 100644 --- a/cloud-recording/develop/individual-mode.md +++ b/cloud-recording/develop/individual-mode.md @@ -40,16 +40,16 @@ Before recording, call the [`acquire`](../reference/rest-api/acquire) method to - Request URL: - ``` json + ```json https://api.agora.io/v1/apps//cloud_recording/acquire - ``` + ``` - `Content-type`: `application/json;charset=utf-8` - `Authorization`: Basic authorization. For more information, see [How to pass the basic HTTP authentication](../reference/restful-authentication). - Request body: - ``` json + ```json { "cname": "https://xxxxx", "uid": "527841", @@ -79,7 +79,7 @@ In individual recording mode, you can configure the following parameters in `cli #### An HTTP request example of `start` - Request URL: - ``` json + ```json https://api.agora.io/v1/apps//cloud_recording/resourceid//mode/individual/start ``` - `Content-type`: `application/json;charset=utf-8` @@ -90,7 +90,7 @@ In individual recording mode, you can configure the following parameters in `cli **Real-time recording for standard mode** -``` json +```json { "uid": "527841", "cname": "httpClient463224", @@ -133,7 +133,7 @@ When a recording finishes, call [`stop`](../reference/rest-api/stop) to leave th #### An HTTP request example of `stop` - The request URL is: - ``` json + ```json http://api.agora.io/v1/apps//cloud_recording/resourceid//sid//mode/individual/stop ``` - `Content-type`: `application/json;charset=utf-8` @@ -143,7 +143,7 @@ When a recording finishes, call [`stop`](../reference/rest-api/stop) to leave th - Request body: - ``` json + ```json { "cname": "httpClient463224", "uid": "527841", diff --git a/cloud-recording/develop/integration-best-practices.md b/cloud-recording/develop/integration-best-practices.md index ad28156ef..ef56871d9 100644 --- a/cloud-recording/develop/integration-best-practices.md +++ b/cloud-recording/develop/integration-best-practices.md @@ -15,7 +15,47 @@ To improve application robustness, Agora recommends that you do the following wh If you send a Cloud Recording RESTful API request to `api.agora.io` and the request fails, retry with the same domain name first. If it fails again, replace the domain name with `api.sd-rtn.com` and retry. Best practice is to first try the DNS domain close to your server. See the [domain name table](#domain-name-table) for a list of DNS servers. -Agora recommends that you use a backoff strategy, for example, retry after 1, 3, and 6 seconds successively, to avoid exceeding the Queries Per Second (QPS) limits. +Agora recommends that you use a backoff strategy, for example, retry after 1, 3, and 6 seconds successively, to avoid exceeding the Queries Per Second limits. + +## Check the limits + +Check that your Peak Concurrent Worker (PCW), Queries Per Second (QPS), and the number of streams do not exceed the following limits set by Agora. + +#### PCW + +The PCW limit depends on your video stream resolution and region. + +Resolutions: + +- SD: Standard definition video, resolution ≤ 640 × 360 +- HD: High definition video, resolution ≤ 1280 × 720 and > 640 × 360 +- FHD: Full HD video, resolution ≤ 1920 × 1080 and > 1280 × 720 + +| Service type | Mainland China | Europe | America | Asia (excluding mainland China) | +|:---------------------|:-----------------------|:---------|:--------------|:--------------------------------| +| Individual recording | 1000 | 200 | 400 | 300 | +| Composite recording |

    |
    • SD 50
    • HD 30
    • FHD 10
    |
    • SD 100
    • HD 50
    • FHD 30
    |
    • SD 100
    • HD 50
    • FHD 30
    | + +If you need to extend the PCW limit, please contact support@agora.io. + +#### QPS + +The initial QPS limit is 10 per App ID when you register. You can estimate the QPS that your project needs based on your PCW value and query frequency. If you need to extend the limit for QPS, contact support@agora.io. + +#### Number of streams + +The upper limit of video attributes supported by Agora is as follows: + +- Resolution 1920 × 1080 +- Frame rate 30 FPS + +The maximum number of supported streams is as follows: + +| Service type | Mainland China | Europe | America | Asia (excluding mainland China) | +|:----------------|:----------------------|:---------------------|:----------------------|:--------------------------------| +| Cloud recording |
    • SD 100
    • HD 50
    • FHD 30
    |
    • SD 50
    • HD 30
    • FHD 10
    |
    • SD 100
    • HD 50
    • FHD 30
    |
    • SD 100
    • HD 50
    • FHD 30
    | + +If you need to record multiple streams of different resolutions at the same time, make sure you meet the following requirements:
    • The number of streams per resolution cannot exceed the corresponding limit for that resolution.
    • The total number of streams cannot exceed the limit set for the higher resolution. For example, if you need to use cloud recording in Europe to record in both SD and HD, the total number of streams cannot exceed 50. If you record in both HD and FHD, the total number cannot exceed 30.
    ## Get service status @@ -23,8 +63,6 @@ You use Cloud Recording RESTful APIs to get the status of the recording service. Best practice is that core apps do not rely on (). If your apps already rely heavily on the , contact
    support@agora.io and enable the redundant message notification function. This doubles the received notifications and reduces the probability of message loss. After enabling the message notification function, you need to deduplicate messages based on `sid`. Message notification still cannot guarantee a 100% arrival rate. -The initial QPS limit is 10 per App ID when you register. You can estimate the QPS quota your project needs according to your Peak Concurrent Worker (PCW) quota and query frequency. The initial PCW limit is 50 per AppID when you register. If the RESTful API returns QPS limitation error code `429`, or PCW quota limitation error code `406`, then retry, or contact support@agora.io to increase your QPS or PCW quota. - ### Ensure the recording service starts successfully Take the following steps to ensure that the recording service starts successfully: @@ -95,7 +133,7 @@ To guarantee high availability of important scenes with a large audience, best p 1. Use Notifications to [Handle notifications for specific events](/en/cloud-recording/develop/receive-notifications#cloud-recording-callback-events). After starting the recording, if you don't receive event `13` `High availability register success` within 10 seconds, create a new recording task with a different UID. -These fault recovery methods may result in multiple recording tasks. You are charged separately for each task. For more information, see [Pricing](../reference/pricing). +These fault recovery methods may result in multiple recording tasks. You are charged separately for each task. For more information, see [Pricing](../overview/pricing). diff --git a/cloud-recording/develop/recording-video-profile.md b/cloud-recording/develop/recording-video-profile.md index 0d94aa98f..c342f5ed1 100644 --- a/cloud-recording/develop/recording-video-profile.md +++ b/cloud-recording/develop/recording-video-profile.md @@ -13,7 +13,7 @@ In individual recording mode, the recorded video keeps the original video profil ## Basic guidelines -- Agora recommends setting the recording resolution lower than the [aggregate resolution](../reference/pricing#resolution-calibration) of the original video streams, otherwise the recorded video may be blurry. +- Agora recommends setting the recording resolution lower than the [aggregate resolution](../overview/pricing#resolution-calibration) of the original video streams, otherwise the recorded video may be blurry. - The resolution you set in the video profile is that of the video canvas, and its aspect ratio does not need to be identical to any source video stream. The aspect ratio of each user region in the output video depends on the aspect ratio of the canvas and the video layout. See [Related articles](#related-articles). - Agora only supports the following frame rates: 1 fps, 7 fps, 10 fps, 15 fps, 24 fps, 30 fps, and 60 fps. The default value is 15 fps. If you set other frame rates, the SDK uses the default value. - The base bitrate in the video profile table applies to the communication profile. The live-broadcast profile generally requires a higher bitrate to ensure better video quality. Set the bitrate of the live-broadcast profile as twice the base bitrate. diff --git a/cloud-recording/develop/screen-capture.md b/cloud-recording/develop/screen-capture.md index 79161b2b6..423b12f3b 100644 --- a/cloud-recording/develop/screen-capture.md +++ b/cloud-recording/develop/screen-capture.md @@ -1,5 +1,5 @@ --- -title: "Capture screenshots" +title: "Cloud-based screenshot upload" sidebar_position: 11 type: docs platform_selector: false @@ -15,7 +15,7 @@ The following two screenshot methods are supported: - Take screenshots only. - Capture screenshots and recording during a recording process. Agora only charges recording fees. -For pricing details, see [Pricing](../reference/pricing). +For pricing details, see [Pricing](../overview/pricing). To implement client-side screen capture, see [Screenshot Upload](../../video-calling/enable-features/screenshot-upload). diff --git a/cloud-recording/develop/webpage-best-practices.md b/cloud-recording/develop/webpage-best-practices.md index 024d7218c..8c950a854 100644 --- a/cloud-recording/develop/webpage-best-practices.md +++ b/cloud-recording/develop/webpage-best-practices.md @@ -21,6 +21,45 @@ External factors can cause problems with web page recording, including the follo To ensure reliability and consistency in the face of network issues, Agora recommends the following best practices. +### Check the limits + +Check that your Peak Concurrent Worker (PCW), Queries Per Second (QPS), and the number of streams do not exceed the following limits set by Agora. + +#### PCW + +The PCW limit depends on your video stream resolution and region. + +Resolutions: + +- SD: Standard definition video, resolution ≤ 640 × 360 +- HD: High definition video, resolution ≤ 1280 × 720 and > 640 × 360 +- FHD: Full HD video, resolution ≤ 1920 × 1080 and > 1280 × 720 + +| Service type | Mainland China | Europe | America | Asia (excluding mainland China) | +|:---------------------|:------------------------|:-------------------|:--------------------------------|:------------| +| Web page recording |
    • SD 100
    • HD 50
    • FHD 30
    |
    • SD 50
    • HD 30
    • FHD 10
    |
    • SD 100
    • HD 50
    • FHD 30
    |
    • SD 100
    • HD 50
    • FHD 30
    | + +If you need to extend the PCW limit, contact support@agora.io. + +#### QPS + +The initial QPS limit is 10 per App ID when you register. You can estimate the QPS that your project needs based on your PCW value and query frequency. If you need to extend the limit for QPS, contact support@agora.io. + +#### Number of streams + +The upper limit of video attributes supported by Agora is as follows: + +- Resolution 1920 × 1080 +- Frame rate 30 FPS + +The maximum number of supported streams is as follows: + +| Service type | Mainland China | Europe | America | Asia (excluding mainland China) | +|:-------------------|:----------------------|:---------------------|:----------------------|:---------------------------------------------------------| +| Web page recording |
    • SD 100
    • HD 50
    • FHD 30
    |
    • SD 50
    • HD 30
    • FHD 10
    |
    • SD 100
    • HD 50
    • FHD 30
    |
    • SD 100
    • HD 100
    • FHD 30
    | + +If you need to record multiple streams of different resolutions at the same time, make sure you meet the following requirements:
    • The number of streams per resolution cannot exceed the corresponding limit for that resolution.
    • The total number of streams cannot exceed the limit set for the higher resolution. For example, if you need to record in America in both SD and HD, the total number of streams cannot exceed 100. If you record in both HD and FHD, the total number cannot exceed 50.
    + ### Ensure the recording service starts successfully Take the following steps to ensure that the recording service starts successfully: diff --git a/cloud-recording/develop/webpage-mode.md b/cloud-recording/develop/webpage-mode.md index d027fe728..6a5583f26 100644 --- a/cloud-recording/develop/webpage-mode.md +++ b/cloud-recording/develop/webpage-mode.md @@ -302,7 +302,7 @@ A web page recording session generates one M3U8 file and multiple TS files. Depe ## Pricing -Web page recording mode is free to use by November 1, 2021. See [Pricing for Web Page Recording](../reference/pricing-webpage-recording) for details. +Web page recording mode is free to use by November 1, 2021. See [Pricing for Web Page Recording](../overview/pricing-webpage-recording) for details. ## Considerations diff --git a/cloud-recording/get-started/getstarted.md b/cloud-recording/get-started/getstarted.md index ccfe00b07..7dd17040a 100644 --- a/cloud-recording/get-started/getstarted.md +++ b/cloud-recording/get-started/getstarted.md @@ -30,7 +30,7 @@ After the recording is over, the cloud recording service uploads the recording f ## Prerequisites -- A valid [Agora account](https://console.agora.io/). +- A valid Agora Account. - A valid Agora project with an App ID and a temporary token. For details, see [Get Started with Agora](../reference/manage-agora-account#get-the-app-id). - A computer with access to the internet. If your network has a firewall, follow the instructions in [Firewall Requirements](../reference/firewall). - Ensure that a third-party cloud storage service has been enabled. The currently supported third-party cloud storage service providers are as follows: @@ -49,7 +49,7 @@ After the recording is over, the cloud recording service uploads the recording f Enable the cloud recording service before using Agora Cloud Recording for the first time. -1. Log in to [Agora Console](https://console.agora.io/), and click the **Project Management** icon on the left navigation panel. +1. Log in to , and click the **Project Management** icon on the left navigation panel. 2. On the **Project Management** page, find the project for which you want to enable the cloud recording service, and click the edit icon. 3. On the **Edit Project** page, find **Cloud Recording**, and click **Enable**. ![](https://web-cdn.agora.io/docs-files/1638866909361) diff --git a/cloud-recording/reference/pricing-webpage-recording.md b/cloud-recording/overview/pricing-webpage-recording.md similarity index 94% rename from cloud-recording/reference/pricing-webpage-recording.md rename to cloud-recording/overview/pricing-webpage-recording.md index fa8ef898d..d21b94e50 100644 --- a/cloud-recording/reference/pricing-webpage-recording.md +++ b/cloud-recording/overview/pricing-webpage-recording.md @@ -1,6 +1,6 @@ --- title: "Pricing for Web Page Recording" -sidebar_position: 2 +sidebar_position: 4 type: docs platform_selector: false description: > @@ -67,7 +67,7 @@ following categories: ## Preferential billing policies If Video SDK is used in the web page being recorded to implement real-time communications, and the user is -subscribed to a channel with a high-definition (HD) [aggregate video resolution](../reference/pricing#aggregate), Agora waives the cost of the video usage during the web page recording; only the web page recording fees apply. Real-time communication at higher aggregate resolutions does not receive this discount. +subscribed to a channel with a high-definition (HD) [aggregate video resolution](pricing#aggregate), Agora waives the cost of the video usage during the web page recording; only the web page recording fees apply. Real-time communication at higher aggregate resolutions does not receive this discount. ## Examples @@ -110,4 +110,4 @@ actual business scenario or actively stop the web page recording. - [Agora's free-of-charge policy for the first 10,000 minutes](../reference/billing-policies#agoras-free-of-charge-policy-for-the-first-10000-minutes) - [Billing, free deduction, and account suspension](../reference/billing-policies#billing-fee-deductions-and-account-suspension-policies) -- [Cloud Recording pricing](../reference/pricing) \ No newline at end of file +- [Cloud Recording pricing](pricing) \ No newline at end of file diff --git a/cloud-recording/reference/pricing.md b/cloud-recording/overview/pricing.md similarity index 97% rename from cloud-recording/reference/pricing.md rename to cloud-recording/overview/pricing.md index 17a590baa..1ccb47501 100644 --- a/cloud-recording/reference/pricing.md +++ b/cloud-recording/overview/pricing.md @@ -1,6 +1,6 @@ --- -title: "Pricing" -sidebar_position: 1 +title: "Pricing for Cloud Recording" +sidebar_position: 3 type: docs platform_selector: false description: > @@ -22,10 +22,9 @@ The unit pricing for audio and video usage is as follows: | Recording Video | Full High-Definition (Full HD) | 13.49 | | Recording Video | 2K | 23.99 | | Recording Video | 2K+ | 53.99 | -| Option: Standalone screenshot by video | < 720P | 5.99 | -| Option: Standalone screenshot by video | Full HD (720P - 1080P) | 13.49 | -| Option: Standalone screenshot by video | 2K (1080P to 2K) | 23.99 | +| Cloud-Based Screenshot Upload | All definitions | 2.49 or free | +**Note**: Cloud-Based Screenshot Upload is free if bundled with Cloud Recording, that is, initiated along with Cloud Recording in a single task. Agora determines video category based on **aggregate video resolution**, which is the sum of resolutions of all the video streams a user subscribes to at the same time. Agora adds up the resolution of all the video streams recorded at the same @@ -50,7 +49,7 @@ but affects the aggregate recording resolution. Billing for Cloud Recording begins once you use Cloud Recording to record and save audio calls, group video calls, or interactive video streaming made via the Agora on your cloud storage. -Agora calculates the billing of all projects under your [Agora account](https://console.agora.io/) monthly. +Agora calculates the billing of all projects under your Agora Account monthly. After deducting the monthly [10,000 free-of-charge minutes](../reference/billing-policies#agoras-free-of-charge-policy-for-the-first-10000-minutes) that Agora grants to every account, Agora adds up the usage duration (in seconds) of audio and video in each category, and divides them by 60 to get the respective service minutes (rounded up to the nearest integer). @@ -307,4 +306,4 @@ When calculating the aggregate resolution, Agora counts the resolution of 225,28 - [Agora's free-of-charge policy for the first 10,000 minutes](../reference/billing-policies#agoras-free-of-charge-policy-for-the-first-10000-minutes) - [Billing, free deduction, and account suspension](../reference/billing-policies#billing-fee-deductions-and-account-suspension-policies) -- [Web Page Recording pricing](../reference/pricing-webpage-recording) \ No newline at end of file +- [Web Page Recording pricing](pricing-webpage-recording) \ No newline at end of file diff --git a/cloud-recording/overview/product-overview.mdx b/cloud-recording/overview/product-overview.mdx index 2c0d75aa0..d6f76ba18 100644 --- a/cloud-recording/overview/product-overview.mdx +++ b/cloud-recording/overview/product-overview.mdx @@ -10,7 +10,7 @@ description: > diff --git a/cloud-recording/reference/supported-platforms.mdx b/cloud-recording/overview/supported-platforms.mdx similarity index 93% rename from cloud-recording/reference/supported-platforms.mdx rename to cloud-recording/overview/supported-platforms.mdx index 47f841026..c1ebc9f81 100644 --- a/cloud-recording/reference/supported-platforms.mdx +++ b/cloud-recording/overview/supported-platforms.mdx @@ -1,6 +1,6 @@ --- title: 'Supported platforms' -sidebar_position: 7 +sidebar_position: 6 type: docs platform_selector: false description: > diff --git a/cloud-recording/reference/rest-api/start.md b/cloud-recording/reference/rest-api/start.md index 2992469a6..e8f27bf29 100644 --- a/cloud-recording/reference/rest-api/start.md +++ b/cloud-recording/reference/rest-api/start.md @@ -231,7 +231,7 @@ Agora supports only taking screenshots in a recording process or recording and t - `23`:US_GOV_EAST_1 - `24`: AP_SOUTHEAST_3 - `25`: EU_SOUTH_1 - + - `28`: IL_CENTRAL_1 - Third-party cloud storage is Alibaba Cloud (`vendor` = 2): - `0`: CN_Hangzhou - `1`: CN_Shanghai diff --git a/extensions-marketplace/develop/integrate/banuba.mdx b/extensions-marketplace/develop/integrate/banuba.mdx index c211fdcde..c21e76c6d 100644 --- a/extensions-marketplace/develop/integrate/banuba.mdx +++ b/extensions-marketplace/develop/integrate/banuba.mdx @@ -51,7 +51,7 @@ To receive a trial token or a full commercial licence from Banuba - please fill 4. Copy and Paste your Banuba client token into the appropriate section of `/BanubaAgoraFilters/Token.swift` with “ ” symbols. For example: - ```` swift + ````swift let banubaClientToken = "Banuba Token" ```` @@ -59,7 +59,7 @@ To receive a trial token or a full commercial licence from Banuba - please fill 6. Copy and Paste your agora token, app and chanel ID into appropriate section of `/BanubaAgoraFilters/Token.swift` with “ ” symbols. For example: - ```` swift + ````swift internal let agoraAppID = "Agora App ID" internal let agoraClientToken = "Agora Token" internal let agoraChannelId = "Agora Channel ID" diff --git a/extensions-marketplace/develop/integrate/byteplus.mdx b/extensions-marketplace/develop/integrate/byteplus.mdx index 93ecc15bb..56cc740b2 100644 --- a/extensions-marketplace/develop/integrate/byteplus.mdx +++ b/extensions-marketplace/develop/integrate/byteplus.mdx @@ -94,7 +94,7 @@ The BytePlus Effects extension works together with the , download the iOS package of **BytePlus Effects**. 3. Unzip the package, and save all `.framework` files to the `` path. 4. Contact Agora to get the resource package of the BytePlus extension. Save the files you need to the `Resource` path. For details, see [Resource package structure](https://docs.byteplus.com/effects/docs/resource-package-structure-v421-and-later). 5. Import the required header files: diff --git a/extensions-marketplace/develop/integrate/livedata-conversation-intelligence.mdx b/extensions-marketplace/develop/integrate/livedata-conversation-intelligence.mdx new file mode 100644 index 000000000..0cfe851c0 --- /dev/null +++ b/extensions-marketplace/develop/integrate/livedata-conversation-intelligence.mdx @@ -0,0 +1,12 @@ +--- +title: "LiveData Conversation Intelligence" +sidebar_position: 8 +type: docs +description: > + Integrate and use the LiveData Conversation Intelligence extension in your app. +--- +export const toc = [{}]; + +import LiveDataConversationIntelligence from '@docs/shared/extensions-marketplace/livedata-conversation-intelligence/index.mdx'; + + \ No newline at end of file diff --git a/extensions-marketplace/develop/integrate/superclarity.mdx b/extensions-marketplace/develop/integrate/superclarity.mdx deleted file mode 100644 index 14691db1f..000000000 --- a/extensions-marketplace/develop/integrate/superclarity.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: "Super Clarity" -sidebar_position: 7 -type: docs -description: > - Super clarity feature for Agora Video SDK for Web. ---- - -import SuperClarity from '@docs/shared/extensions-marketplace/_superclarity.mdx'; - -export const toc = [{}]; - - diff --git a/extensions-marketplace/develop/integrate/symbl_ai.mdx b/extensions-marketplace/develop/integrate/symbl_ai.mdx index e4d678782..162de80ea 100644 --- a/extensions-marketplace/develop/integrate/symbl_ai.mdx +++ b/extensions-marketplace/develop/integrate/symbl_ai.mdx @@ -1,6 +1,6 @@ --- title: "Symbl Conversation Intelligence" -sidebar_position: 8 +sidebar_position: 9 type: docs description: > Enable augmented reality features in video calls such as face filters, face touch up filters and virtual backgrounds. @@ -137,7 +137,7 @@ You would have to call the method above during the initialization of the Agora E On your `onEvent()` method, you should receive all the transcription information and the analyzed conversation events where you can parse the JSON responses. -For a basic speech-to-text (recognition result) type of response, you should expect the following payload: +For a basic Speech-to-Text (recognition result) type of response, you should expect the following payload: ````json { diff --git a/extensions-marketplace/develop/integrate/synervoz.mdx b/extensions-marketplace/develop/integrate/synervoz.mdx index 7d3d4b416..3fd4b66ec 100644 --- a/extensions-marketplace/develop/integrate/synervoz.mdx +++ b/extensions-marketplace/develop/integrate/synervoz.mdx @@ -1,6 +1,6 @@ --- title: "Synervoz Voice FX" -sidebar_position: 9 +sidebar_position: 10 type: docs description: > Provides the following voice filters to users: echo, reverb, flanger and pitch shift. diff --git a/extensions-marketplace/reference/release-notes.mdx b/extensions-marketplace/overview/release-notes.mdx similarity index 94% rename from extensions-marketplace/reference/release-notes.mdx rename to extensions-marketplace/overview/release-notes.mdx index 62af2a52d..8844b98ac 100644 --- a/extensions-marketplace/reference/release-notes.mdx +++ b/extensions-marketplace/overview/release-notes.mdx @@ -1,6 +1,6 @@ --- title: 'Release notes' -sidebar_position: 2 +sidebar_position: 3 type: docs description: > Information about changes in the releases of different extensions. diff --git a/extensions-marketplace/reference/supported-platforms.mdx b/extensions-marketplace/overview/supported-platforms.mdx similarity index 99% rename from extensions-marketplace/reference/supported-platforms.mdx rename to extensions-marketplace/overview/supported-platforms.mdx index 576a2513d..d27ff17cb 100644 --- a/extensions-marketplace/reference/supported-platforms.mdx +++ b/extensions-marketplace/overview/supported-platforms.mdx @@ -1,6 +1,6 @@ --- title: 'Supported platforms' -sidebar_position: 5 +sidebar_position: 4 type: docs description: > The platforms supported by this product. diff --git a/real-time-transcription/reference/_category_.json b/flexible-classroom/client-api/_category_.json similarity index 70% rename from real-time-transcription/reference/_category_.json rename to flexible-classroom/client-api/_category_.json index d540fd6db..b0e665819 100644 --- a/real-time-transcription/reference/_category_.json +++ b/flexible-classroom/client-api/_category_.json @@ -1,6 +1,6 @@ { "position": 4, - "label": "Reference", + "label": "Client API", "collapsible": true, "link": null } diff --git a/flexible-classroom/reference/classroom-sdk.mdx b/flexible-classroom/client-api/classroom-sdk.mdx similarity index 100% rename from flexible-classroom/reference/classroom-sdk.mdx rename to flexible-classroom/client-api/classroom-sdk.mdx diff --git a/flexible-classroom/reference/edu-context-sdk.mdx b/flexible-classroom/client-api/edu-context-sdk.mdx similarity index 100% rename from flexible-classroom/reference/edu-context-sdk.mdx rename to flexible-classroom/client-api/edu-context-sdk.mdx diff --git a/flexible-classroom/reference/proctor-sdk.mdx b/flexible-classroom/client-api/proctor-sdk.mdx similarity index 100% rename from flexible-classroom/reference/proctor-sdk.mdx rename to flexible-classroom/client-api/proctor-sdk.mdx diff --git a/flexible-classroom/reference/ui-scene.mdx b/flexible-classroom/client-api/ui-scene.mdx similarity index 91% rename from flexible-classroom/reference/ui-scene.mdx rename to flexible-classroom/client-api/ui-scene.mdx index 43fc00ff6..fe5696ac3 100644 --- a/flexible-classroom/reference/ui-scene.mdx +++ b/flexible-classroom/client-api/ui-scene.mdx @@ -1,5 +1,5 @@ --- -title: 'UI Scene SDK' +title: 'FcrUIScene SDK' sidebar_position: 5 type: docs description: > diff --git a/flexible-classroom/develop/authentication-workflow.mdx b/flexible-classroom/develop/authentication-workflow.mdx index 090f837e1..343f821cf 100644 --- a/flexible-classroom/develop/authentication-workflow.mdx +++ b/flexible-classroom/develop/authentication-workflow.mdx @@ -34,9 +34,9 @@ a token is a dynamic key generated on your app server that is val In order to follow this procedure you must have the following: -- A valid [ account](../reference/manage-agora-account#_create_an_agora_account). +- A valid [ account](../get-started/manage-agora-account#_create_an_agora_account). -- An Agora project with the [App Certificate](../reference/manage-agora-account#_get_the_app_certificate) enabled. +- An Agora project with the [App Certificate](../get-started/manage-agora-account#_get_the_app_certificate) enabled. - [Golang](https://golang.org/) 1.14+ with GO111MODULE set to on. @@ -58,13 +58,13 @@ This section shows you how to get the security information needed to generate a Agora automatically assigns each project an App ID as a unique identifier. -To copy this App ID, find your project on the [Project Management](https://console.agora.io/projects) page in Agora Console, and click the plus icon in the App ID column. +To copy this App ID, find your project on the Project Management page in Agora Console, and click the copy icon in the App ID column. #### 2. Get the App Certificate To get an App Certificate, do the following: -1. On the [Project Management](https://console.agora.io/projects) page, click **Config** for the project you want to use. +1. On the Project Management page, click **Config** for the project you want to use. ![1641971710869](https://web-cdn.agora.io/docs-files/1641971710869) 2. Click the copy icon under **Primary Certificate**. @@ -84,7 +84,7 @@ In order to show the authentication workflow, this section shows how to build an - **AccessToken2** - ``` go + ```go package main import ( @@ -190,7 +190,7 @@ In order to show the authentication workflow, this section shows how to build an ``` - **AccessToken** - ``` go + ```go package main import ( @@ -296,19 +296,19 @@ In order to show the authentication workflow, this section shows how to build an 2. A `go.mod` file defines this module’s import path and dependency requirements. To create the `go.mod` for your token server, run the following command: - ``` shell + ```shell $ go mod init sampleServer ``` 3. Get dependencies by running the following command: - ``` shell + ```shell $ go get ``` 4. Start the server by running the following command: - ``` shell + ```shell $ go run server.go ``` @@ -326,11 +326,11 @@ In order to show the authentication workflow, this section shows how to build an - `client.js`: App logic with Agora -2. Download [Agora for Web](../reference/downloads). Save the JS file in `libs` to your project directory. +2. Download [Agora for Web](../overview/downloads). Save the JS file in `libs` to your project directory. 3. In `index.html`, add the following code to include the app logic in the UI, then replace `` with the path of the JS file you saved in step 2. - ``` html + ```html Signaling token demo @@ -346,7 +346,7 @@ In order to show the authentication workflow, this section shows how to build an 4. Create the app logic by editing `client.js` with the following content. Then replace `` with your App ID. The App ID must match the one in the server. You also need to replace `` with the host URL and port of the local Golang server you have just deployed, such as `10.53.3.234:8082`. - ``` js + ```js // Parameters for the login method let options = { token: "", @@ -499,7 +499,7 @@ This section introduces the method to generate a token. Take C++ - **AccessToken2** - ``` go + ```go func BuildToken(appId string, appCertificate string, userId string, expire uint32) (string, error) { token := accesstoken.NewAccessToken(appId, appCertificate, expire) serviceRtm := accesstoken.NewServiceRtm(userId) @@ -518,7 +518,7 @@ This section introduces the method to generate a token. Take C++ - **AccessToken** - ``` cpp + ```cpp static std::string buildToken(const std::string& appId, const std::string& appCertificate, const std::string& userAccount, @@ -547,7 +547,7 @@ This section introduces how to upgrade from AccessToken to AccessToken2 by examp 1. Replace the `rtmtokenbuilder` import statement: -``` go +```go // Replace "github.com/AgoraIO/Tools/DynamicKey/AgoraDynamicKey/go/src/RtmTokenBuilder" // with "github.com/AgoraIO/Tools/DynamicKey/AgoraDynamicKey/go/src/rtmtokenbuilder2". import ( @@ -564,7 +564,7 @@ import ( 2. Update the `BuildToken` function: -``` go +```go // Previously, it is `result, err := rtmtokenbuilder.BuildToken(appID, appCertificate, rtm_uid, rtmtokenbuilder.RoleRtmUser, expireTimestamp)`. // Now, remove `rtmtokenbuilder.RoleRtmUser`. result, err := rtmtokenbuilder.BuildToken(appID, appCertificate, rtm_uid, expireTimestamp) diff --git a/flexible-classroom/develop/classroom-security.md b/flexible-classroom/develop/classroom-security.md index 70b954c33..3b32aa8a7 100644 --- a/flexible-classroom/develop/classroom-security.md +++ b/flexible-classroom/develop/classroom-security.md @@ -26,7 +26,7 @@ Agora creates independent and isolated classrooms for audio, video, or messaging Agora Flexible Classroom uses the token for end user authentication. The token is an access key with the expiration time of 24 hours. It is generated by the app backend with important information such as the Agora App ID, user ID (`uid`), and channel name. It allows end users to access the Agora platform after the user is properly validated by the app. -The app developer can enable token authentication (App Certificate) in [Agora Console](https://console.agora.io/). When enabled, all user’s request to join a classroom must be done with a valid token. +The app developer can enable token authentication (App Certificate) in . When enabled, all user’s request to join a classroom must be done with a valid token. - For more information on how to enable token authentication, see [Use an token for authentication](/signaling/develop/authentication-workflow). - For how to generate an token on the app backend, see [Generate an token](/signaling/develop/authentication-workflow). @@ -58,7 +58,7 @@ With network geofencing enabled, data transfer will be restricted to the service Use this list to quickly check what measures you have or have not taken to best protect the security of your app and users: -1. Enable token authentication in [Agora Console](https://console.agora.io/). +1. Enable token authentication in . 2. Disable **No certificate** in your project management page. Once it is done your app authenticates users with tokens only. ![img](https://web-cdn.agora.io/docs-files/1614134532547) 3. [Deploy an token server](/signaling/develop/authentication-workflow) in your backend services. diff --git a/flexible-classroom/develop/proctor-exams-online.mdx b/flexible-classroom/develop/proctor-exams-online.mdx index 3f17a2741..2a462584b 100644 --- a/flexible-classroom/develop/proctor-exams-online.mdx +++ b/flexible-classroom/develop/proctor-exams-online.mdx @@ -15,7 +15,7 @@ sharing their screens during online exams. This can be useful in the following c provides to implement such supervision. Agora recommends a scenario where one examiner proctors 25 to 50 examinees at the same time. This page illustrates how the proctoring feature works for an examiner -and an examinee using the [ web demo](./reference/downloads?platform=web). +and an examinee using the [ web demo](./overview/downloads?platform=web). ## Prerequisites @@ -70,7 +70,7 @@ screen sharing. Take the following steps to join and proctor an exam: -1. Open the [ web demo](./reference/downloads?platform=web) and click **Create** to create a room. +1. Open the [ web demo](./overview/downloads?platform=web) and click **Create** to create a room. ![flexible_classroom_web_demo](/images/flexible-classroom/fc_web_demo.png) @@ -116,7 +116,7 @@ The test ends automatically when the test time is over, or you can manually clic Take the following steps to join and take an exam in a supervised environment: -1. Open the [ web demo](./reference/downloads?platform=web) and click **Join**. +1. Open the [ web demo](./overview/downloads?platform=web) and click **Join**. ![join_classroom](/images/flexible-classroom/join_classroom.png) @@ -149,7 +149,7 @@ This section contains information that completes the information in this page, o ### API Reference -- [](http://localhost:3000/en/flexible-classroom/reference/proctor-sdk) +- [](docs.agora.io/en/flexible-classroom/client-api/proctor-sdk) diff --git a/flexible-classroom/develop/record-a-class.mdx b/flexible-classroom/develop/record-a-class.mdx index 418eab20f..c37e23db2 100644 --- a/flexible-classroom/develop/record-a-class.mdx +++ b/flexible-classroom/develop/record-a-class.mdx @@ -19,7 +19,7 @@ In Flexible Classroom, users normally start recording manually. The process is a 1. The server opens a browser window and navigates to the address specified in `recordURL` configured in `launchOption`. 1. The server starts recording. -If you want the recording to start automatically, you can listen for the event of class starting on the server side, and call [Set the recording state](../reference/classroom-api#set-the-recording-state) to start automatic recording. +If you want the recording to start automatically, you can listen for the event of class starting on the server side, and call [Set the recording state](../restful-api/classroom-api#set-the-recording-state) to start automatic recording. If you want to implement recording on your own, you can refer to the following diagram for the process. The steps highlighted in purple need to be implemented by you. @@ -31,10 +31,10 @@ When you deploy the web page to be recorded into your CDN, you can use the templ ## Start the recording -Whether you initiate the recording on the client or server, call [Set the recording state](../reference/classroom-api#set-the-recording-state). When calling this method, pay attention to the following parameters: +Whether you initiate the recording on the client or server, call [Set the recording state](../restful-api/classroom-api#set-the-recording-state). When calling this method, pay attention to the following parameters: - `mode`: Set this parameter as `web` to enable [web page recording](../../cloud-recording/develop/webpage-mode). -- `rootUrl`: The root address of the web page to be recorded. The Flexible Classroom cloud service automatically gets the full address of the web page to be recorded by appending `roomUuid`, `roomType`, and other parameters after the root address. You need to extract this information from the URL and pass it in when calling the [launch](../reference/classroom-sdk#launch) method. +- `rootUrl`: The root address of the web page to be recorded. The Flexible Classroom cloud service automatically gets the full address of the web page to be recorded by appending `roomUuid`, `roomType`, and other parameters after the root address. You need to extract this information from the URL and pass it in when calling the [launch](../client-api/classroom-sdk#launch) method. - `retryTimeout`: The amount of time (seconds) that the Flexible Classroom cloud service waits between attempts to begin recording. The Flexible Classroom cloud service retries a maximum of two times. Sample code: @@ -50,7 +50,7 @@ Sample code: } ``` -After setting `retryTimeout`, when calling the [launch](../reference/classroom-sdk#launch) method, you need to set the `listener` parameter to listen for the `1` event, which represents that the page has been loaded. When this event is triggered, you need to call the following method to inform the Flexible Classroom cloud service. If the Flexible Classroom cloud service does not receive this notification within `retryTimeout`, it retries the recording. +After setting `retryTimeout`, when calling the [launch](../client-api/classroom-sdk#launch) method, you need to set the `listener` parameter to listen for the `1` event, which represents that the page has been loaded. When this event is triggered, you need to call the following method to inform the Flexible Classroom cloud service. If the Flexible Classroom cloud service does not receive this notification within `retryTimeout`, it retries the recording. - Prototype @@ -95,7 +95,7 @@ After setting `retryTimeout`, when calling the [launch](../reference/classroom-s ## Get the recording state -After starting the recording, the Flexible Classroom cloud service generates an event to indicate the [recording state change](../reference/classroom-api#the-recording-state-changes). You can get the recording state by calling [Query a specified event](../reference/classroom-api#query-a-specified-event) or [Get classroom events](../reference/classroom-api#get-classroom-events). Pay attention to the `reason` field in the recording state change event: +After starting the recording, the Flexible Classroom cloud service generates an event to indicate the [recording state change](../restful-api/classroom-api#the-recording-state-changes). You can get the recording state by calling [Query a specified event](../reference/classroom-api#query-a-specified-event) or [Get classroom events](../reference/classroom-api#get-classroom-events). Pay attention to the `reason` field in the recording state change event: - `1`: Start the recording normally. - `2`: Stop the recording normally. @@ -103,14 +103,14 @@ After starting the recording, the Flexible Classroom cloud service generates an - `4`: Time out. Wait for retry. - `5`: Exit the recording when the number of retries reaches the upper limit. -The clients also receive callbacks that indicate the recording state change in [room properties](../reference/classroom-api#the-recording-state-changes). You can further implement your own logic based on the recording state change. +The clients also receive callbacks that indicate the recording state change in [room properties](../restful-api/classroom-api#the-recording-state-changes). You can further implement your own logic based on the recording state change. ## Remove the white screen at the beginning of the recorded file It takes a while for the recording server to load the web page, but the file slicing begins before the loading finishes. As a result, there may be a period of white screen at the beginning of the recorded file. To remove the white screen, do the following: -1. Before the class starts, call [Set the recording state](../reference/classroom-api#set-the-recording-state), and set `onhold` as `true`. The Flexible Classroom cloud service pauses the recording immediately after the recording task is initiated. The recording server opens and renders the web page, but does not generate a slice file. The following sample code shows this logic: +1. Before the class starts, call [Set the recording state](../restful-api/classroom-api#set-the-recording-state), and set `onhold` as `true`. The Flexible Classroom cloud service pauses the recording immediately after the recording task is initiated. The recording server opens and renders the web page, but does not generate a slice file. The following sample code shows this logic: ```json { @@ -122,7 +122,7 @@ It takes a while for the recording server to load the web page, but the file sli } ``` -2. At least 60 seconds later, call [Update the recording configurations](../reference/classroom-api#update-the-recording-configurations) and set the `onhold` parameter as `false` to start the recording and file slicing. The following sample code shows this logic: +2. At least 60 seconds later, call [Update the recording configurations](../restful-api/classroom-api#update-the-recording-configurations) and set the `onhold` parameter as `false` to start the recording and file slicing. The following sample code shows this logic: ```json { @@ -134,7 +134,7 @@ It takes a while for the recording server to load the web page, but the file sli ## Improve the video clarity when the recorded content is a shared screen -In scenarios where the recorded content is a shared screen or whiteboard, if you have high requirements for video clarity, you can set the following parameters when calling [Set the recording state](../reference/classroom-api#set-the-recording-state): +In scenarios where the recorded content is a shared screen or whiteboard, if you have high requirements for video clarity, you can set the following parameters when calling [Set the recording state](../restful-api/classroom-api#set-the-recording-state): - Set `videoWidth` as 1920. - Set `videoHeight` as 1080. diff --git a/flexible-classroom/develop/supply-course-materials.md b/flexible-classroom/develop/supply-course-materials.md index 005f7ed13..235cd7d59 100644 --- a/flexible-classroom/develop/supply-course-materials.md +++ b/flexible-classroom/develop/supply-course-materials.md @@ -1,5 +1,5 @@ --- -title: "Supply course materials" +title: "Upload course materials" sidebar_position: 6 type: docs description: > @@ -26,54 +26,142 @@ If you want to upload the courseware to third-party cloud storage or to your own 1. On your app server, call this [RESTful API](../../interactive-whiteboard/reference/whiteboard-api/file-conversion#start-file-conversion) to start a file-conversion task. The Agora Interactive Whiteboard service uploads the converted files to the third-party cloud storage that you have configured in Agora Console. -1. On your app server, poll this [RESTful API](../../interactive-whiteboard/reference/whiteboard-api/file-conversion#query-the-progress-of-a-file-conversion-task) to query the progress of a file-conversion task. Pay special attention to the `convertedFileList` parameter in the response. This parameter contains an array of converted files. Each `convertedFileList` object contains the following parameters: - - - `width`: Number. Indicates the width of the image in pixels. - - `height`: Number. Indicates the height of the image in pixels. - - `conversionFileUrl`: String. Indicates the URL of the generated image. - - `preview`: String. Indicates the address of the preview. This field is returned only when `preview` is set as `true` and `type` is set as `dynamic` in the request body when starting file conversion. - -1. When you call [launch](../reference/classroom-sdk#launch) on your client, pass in the list of converted files by setting the [courseWareList](../reference/classroom-sdk#configcourseware) parameter. Then students can see the courseware in the classroom. - - ```json - courseWareList: - [ - { - resourceName: xxxxxxx, - resourceUuid: xxxxxxxxx, - ext: 'pptx', - url: 'https://xxxxxxxxxxxxxx', - size: 0, - updateTime: xxxxxxxx - taskUuid: 'xxxxxxxxx', - conversion: { - type: 'dynamic', - preview: true, - scale: 2, - outputFormat: 'png', - }, - taskProgress: { - totalPageSize: 3, - convertedPageSize: 3, - convertedPercentage: 100, - convertedFileList: [ - { - name: '1', - ppt: { - src: 'pptx://convertcdn.netless.link/dynamicConvert/3bxxxxxxx/1.slide', - width: 1280, - height: 720, - preview:'dddddddddddddddurl' - }, - }, - ... - ] as any, - currentStep: '', - }, - }, - ], +1. On your app server, poll this [RESTful API](../../interactive-whiteboard/reference/whiteboard-api/file-conversion#query-the-progress-of-a-file-conversion-task) to query the progress of a file-conversion task. According to the different types of converted resources, it can be divided into two types, static and dynamic, corresponding to static and dynamic resources, respectively. After a static resource is successfully converted, the structure needs to be converted first and then passed into the launch method. + + Query results returned by the whiteboard resource conversion service: + + ```typescript + { + "uuid":"xxxxxxxxxxx", + "type":"static", + "status":"Finished", + "convertedPercentage":100, + "pageCount":2, + "images":{ + "1":{ + "width":1700, + "height":952, + "url":"https://convertcdn.netless.link/staticConvert/xxx/1.png" + }, + "2":{ + "width":1700, + "height":952, + "url":"https://convertcdn.netless.link/staticConvert/xxx/2.png" + } + } + } ``` + converts to: + + ```typescript + courseWareList: + [ + { + // The file name displayed on the cloud disk + resourceName: xxxxxxx, + // A unique ID + resourceUuid: xxxxxxxxx, + // File name suffix + ext: 'pdf', + // The resources converted by the whiteboard can be left blank. + url: '', + // File size in bytes + size: 0, + // The last update time of the file, in milliseconds + updateTime: xxxxxxxx, + // Pass in the whiteboard resource conversion task ID here + taskUuid: 'xxxxxxxxx', + // Pass in the parameters you passed when you called the whiteboard API to initiate the whiteboard resource conversion task. + conversion: { + type: 'static', + preview: true, + scale: 2, + outputFormat: 'png', + }, + // Task conversion progress needs to bring in the following structural data + taskProgress: { + prefix: "", // The converted resource prefix, if any, is taken from the prefix in the whiteboard conversion result. + // The total number of pages, take the pageCount field in the whiteboard conversion result + totalPageSize: 2, + // The total number of pages, take the pageCount field in the whiteboard conversion result + convertedPageSize: 2, + // Conversion progress, take the convertedPercentage field in the whiteboard conversion result + convertedPercentage: 100, + // Leave array empty + convertedFileList: [], + // Conversion progress, take the status field in the whiteboard conversion result + currentStep: 'Finished', + // Data structure required by static courseware + images: [ + { + // Key in images object + name: '1', + width: 1700, + height: 952, + url:"https://convertcdn.netless.link/staticConvert/xxx/1.png" + }, + { + name: '2', + width: 1700, + height: 952, + url:"https://convertcdn.netless.link/staticConvert/xxx/2.png" + }, + ] + }, + } + ] + ``` + + Dynamic resources do not require additional processing of images and can be passed in through the `courseWareList` method. + +1. When you call [launch](../client-api/classroom-sdk#launch) on your client, pass in the list of converted files by setting the [courseWareList](../reference/classroom-sdk#configcourseware) parameter. Then students can see the courseware in the classroom. + + ```typescript + courseWareList: + [ + { + // The file name displayed on the cloud disk + resourceName: xxxxxxx, + // A unique ID + resourceUuid: xxxxxxxxx, + // File name suffix + ext: 'pptx', + // The resources converted by the whiteboard can be left blank + url: '', + // File size in bytes + size: 0, + // The last update time of the file, in milliseconds + updateTime: xxxxxxxx, + // Pass in the whiteboard resource conversion task ID here + taskUuid: 'xxxxxxxxx', + // Here you need to pass in the parameters you passed when you called the whiteboard API to initiate the whiteboard resource conversion task. + conversion: { + type: 'dynamic', + preview: true, + scale: 2, + outputFormat: 'png', + }, + // Task conversion progress needs to bring in the following structural data + taskProgress: { + prefix: "", // The converted resource prefix, if any, is taken from the prefix in the whiteboard conversion result + // The total number of pages, take the pageCount field in the whiteboard conversion result + totalPageSize: 2, + // The total number of pages, take the pageCount field in the whiteboard conversion result + convertedPageSize: 2, + // Conversion progress, take the convertedPercentage field in the whiteboard conversion result + convertedPercentage: 100, + // Leave array empty + convertedFileList: [], + // Conversion progress, take the status field in the whiteboard conversion result + currentStep: 'Finished', + // The data structure required for static courseware, and the empty array for dynamic courseware + images: [] + }, + }, + ], + ``` + ## Upload courseware during a class To upload courseware during a class, perform the following steps: diff --git a/flexible-classroom/get-started/enable-flexible-classroom.mdx b/flexible-classroom/get-started/enable-flexible-classroom.mdx index 8f8cd489f..e35a0ab3b 100644 --- a/flexible-classroom/get-started/enable-flexible-classroom.mdx +++ b/flexible-classroom/get-started/enable-flexible-classroom.mdx @@ -1,6 +1,6 @@ --- title: "Configure Flexible Classroom" -sidebar_position: 3 +sidebar_position: 2 type: docs description: > Enable the Flexible Classroom service and configure storage and recording in Agora Console. @@ -14,7 +14,7 @@ This page introduces how to enable and configure in [account](../reference/manage-agora-account#create-an-agora-account) and [project](../reference/manage-agora-account#create-an-agora-project). +* An [account](../get-started/manage-agora-account#create-an-agora-account) and [project](../get-started/manage-agora-account#create-an-agora-project). * The whiteboard feature in requires third-party cloud storage. Currently, supports Amazon S3. @@ -22,7 +22,7 @@ In order to follow this procedure you must have: Follow these steps to enable the service in : -1. Log into and navigate to the [Project Management](https://console.agora.io/projects) page. +1. Log into and navigate to the Project Management page. 2. On the **Project Management** page, find the project for which you want to enable the service, and click **Edit**. ![](https://web-cdn.agora.io/docs-files/1641364355621) @@ -191,7 +191,7 @@ To setup and configure storage and recording in for your classrooms: To enable and configure Chat: -1. In , navigate to [Project Management](https://console.agora.io/projects). +1. In , navigate to Project Management. 1. Click **Config** next to the project for which you want to enable Chat. @@ -219,10 +219,10 @@ To enable and configure Chat: ## Considerations -To ensure that Agora can access files in your third-party cloud storage space, you should enable public access or higher permissions for third-party storage spaces. - #### AWS S3 account configuration +To ensure that Agora can access files in your third-party cloud storage space, you should enable public access or higher permissions for third-party storage spaces. + Configure your AWS S3 account as follows: * Bucket policy @@ -267,6 +267,10 @@ Configure your AWS S3 account as follows: ``` ![](/images/flexible-classroom/configure-aws-ss2.png) +#### Service limit + +Flexible Classroom supports a maximum of 10,000 people online at the same time. If you need to extend your limit, contact technical support. + ## Next steps After enabling the service, see how to [quickly launch a classroom](../get-started). diff --git a/flexible-classroom/get-started/get-started-uibuilder.mdx b/flexible-classroom/get-started/get-started-uibuilder.mdx index 1d0ca3953..0871cc9c3 100644 --- a/flexible-classroom/get-started/get-started-uibuilder.mdx +++ b/flexible-classroom/get-started/get-started-uibuilder.mdx @@ -1,6 +1,6 @@ --- title: 'UI Builder quickstart' -sidebar_position: 1 +sidebar_position: 4 description: > Quickly create a custom classroom using UI Builder --- diff --git a/flexible-classroom/get-started/get-started.mdx b/flexible-classroom/get-started/get-started.mdx index 18c0dc8fd..26a96a056 100644 --- a/flexible-classroom/get-started/get-started.mdx +++ b/flexible-classroom/get-started/get-started.mdx @@ -1,6 +1,6 @@ --- title: 'Demo quickstart' -sidebar_position: 2 +sidebar_position: 3 description: > Quickly launch a flexible classroom and experience the features. --- @@ -23,7 +23,7 @@ This section explains the workflow you implement to join a . When an app client requests to join a , the app client and your app server interact with the server in the following steps: 1. Your app client sends a request to your app server for a token. -2. Your app server generates a token using the Agora [App ID](/flexible-classroom/reference/manage-agora-account#get-the-app-id), [App Certificate](/flexible-classroom/reference/manage-agora-account/#get-the-app-certificate), and a user ID. For details, see [Generate a Token]( /flexible-classroom/develop/authentication-workflow/). +2. Your app server generates a token using the Agora [App ID](/flexible-classroom/get-started/manage-agora-account#get-the-app-id), [App Certificate](/flexible-classroom/get-started/manage-agora-account/#get-the-app-certificate), and a user ID. For details, see [Generate a Token]( /flexible-classroom/develop/authentication-workflow/). 3. Your app client calls an API with the following parameters to join a : - *The user ID*: A unique string identifying a user, generated by your security system. This ID must be the same as the user ID you use for generating the token. - *The room ID*: A string for identifying a classroom. When the first user joins a , Agora automatically creates a classroom with the room ID. diff --git a/flexible-classroom/reference/manage-agora-account.mdx b/flexible-classroom/get-started/manage-agora-account.mdx similarity index 94% rename from flexible-classroom/reference/manage-agora-account.mdx rename to flexible-classroom/get-started/manage-agora-account.mdx index cb6421361..68657e9ee 100644 --- a/flexible-classroom/reference/manage-agora-account.mdx +++ b/flexible-classroom/get-started/manage-agora-account.mdx @@ -1,6 +1,6 @@ --- title: 'Agora account management' -sidebar_position: 6 +sidebar_position: 1 type: docs description: > Create, manage and update your Agora account. diff --git a/flexible-classroom/overview/core-concepts.mdx b/flexible-classroom/overview/core-concepts.mdx index 1b6b69a01..21e373644 100644 --- a/flexible-classroom/overview/core-concepts.mdx +++ b/flexible-classroom/overview/core-concepts.mdx @@ -1,6 +1,6 @@ --- title: 'Core concepts' -sidebar_position: 2 +sidebar_position: 3 type: docs platform_selector: false description: > diff --git a/flexible-classroom/reference/downloads.mdx b/flexible-classroom/overview/downloads.mdx similarity index 85% rename from flexible-classroom/reference/downloads.mdx rename to flexible-classroom/overview/downloads.mdx index 7b66bc7c1..56b4833e6 100644 --- a/flexible-classroom/reference/downloads.mdx +++ b/flexible-classroom/overview/downloads.mdx @@ -1,6 +1,6 @@ --- title: 'Samples' -sidebar_position: 3 +sidebar_position: 5 type: docs description: > Links to the manual downloads for this product, and explanations on how to install them. @@ -17,8 +17,8 @@ To demo the full features of the teacher and student across all class types, dow - [**Sample**](https://github.com/AgoraIO-Community/flexible-classroom-desktop/tree/release/2.9.1) - **Demo**: - - [HTML](https://solutions-apaas.agora.io/apaas/demo/index.html#/) - - [HTML5](https://solutions-apaas.agora.io/apaas/demo/index.html#/) + - [HTML](https://solutions-apaas.agora.io/apaas/demo/international/index.html) + - [Web Mobile](https://solutions-apaas.agora.io/apaas/demo/international/index.html) - only supports Lecture Hall scenarios @@ -35,7 +35,10 @@ To demo the full features of the teacher and student across all class types, dow - [Sample](https://github.com/AgoraIO-Community/flexible-classroom-ios/tree/release/2.8.11) -- [Demo](https://testflight.apple.com/join/rYhIIb4L) +- [Demo](https://testflight.apple.com/join/rYhIIb4L) + +![](/images/flexible-classroom/ios-demo-qr.png) + diff --git a/flexible-classroom/reference/pricing.md b/flexible-classroom/overview/pricing.md similarity index 96% rename from flexible-classroom/reference/pricing.md rename to flexible-classroom/overview/pricing.md index 41ca59fc8..b71b84234 100644 --- a/flexible-classroom/reference/pricing.md +++ b/flexible-classroom/overview/pricing.md @@ -1,6 +1,6 @@ --- title: Pricing -sidebar_position: 1 +sidebar_position: 8 description: > Provides you with information on Flexible Classroom pricing --- @@ -52,10 +52,10 @@ If a user subscribes to both audio and video streams, only the video usage is ge The number of free minutes for each service and the billing strategy for the portion exceeding the free quota are detailed in the table below: -| Service item | Usage measurement | Services | Free monthly quota | Overage fee | -|--------------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| | Duration, minutes | Services include classroom functions and server-side functions:
    • Real-time audio and video interaction
    • Interactive whiteboard
    • Signaling
    • Agora Chat
    • Interactive teaching tools
    • Classroom server RESTful API
    |
    • The first 10,000 minutes of services
    • The first 1,000 images of interactive whiteboard document conversion
    • Agora Chat:
      • 10,000 of monthly active users
      • 10,000 automatically generated thumbnails
      • 10 GB of file download traffic
      • 1 GB of file storage
      • 5,000 peak concurrent users
    |
    • [ pricing](#-pricing)
    • [Interactive whiteboard pricing](/interactive-whiteboard/reference/pricing)
    • [Chat pricing](/agora-chat/reference/pricing)
    | -| Web page recording | Duration, minutes | Page recording used during class | The first 10,000 minutes | [Web page recording pricing](/cloud-recording/reference/pricing-webpage-recording) | +| Service item | Usage measurement | Services | Free monthly quota | Overage fee | +|--------------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| | Duration, minutes | Services include classroom functions and server-side functions:
    • Real-time audio and video interaction
    • Interactive whiteboard
    • Signaling
    • Agora Chat
    • Interactive teaching tools
    • Classroom server RESTful API
    |
    • The first 10,000 minutes of services
    • The first 1,000 images of interactive whiteboard document conversion
    • Agora Chat:
      • 10,000 of monthly active users
      • 10,000 automatically generated thumbnails
      • 10 GB of file download traffic
      • 1 GB of file storage
      • 5,000 peak concurrent users
    |
    • [ pricing](#-pricing)
    • [Interactive whiteboard pricing](/interactive-whiteboard/overview/pricing)
    • [Chat pricing](/agora-chat/overview/pricing)
    | +| Web page recording | Duration, minutes | Page recording used during class | The first 10,000 minutes | [Web page recording pricing](/cloud-recording/overview/pricing-webpage-recording) | The free quotas for services and recording services are calculated separately. diff --git a/flexible-classroom/overview/product-features.mdx b/flexible-classroom/overview/product-features.mdx index 9b8c2e32e..445d0c87f 100644 --- a/flexible-classroom/overview/product-features.mdx +++ b/flexible-classroom/overview/product-features.mdx @@ -1,6 +1,6 @@ --- title: 'Flexible classroom features' -sidebar_position: 3 +sidebar_position: 4 type: docs description: > Product architecture, basic and advanced features. @@ -33,4 +33,14 @@ The following figure shows the overall product architecture of : | Device and media |
    • Turns on or off the media devices and conducts device tests.
    • Controls the video rendering.
    • Controls the audio playback.
    | | UIKit/UIStore |
    • Configures multiple languages.
    • Adjusts the classroom layout.
    • Change the classroom colors.
    | | Widget |Implements pluggable widgets, such as the interactive whiteboard, pop-up quiz, and countdown timer. | -| Recording |
    • Configures the resolution of recording files.
    • Configures the storage address of recording files.
    • Configures the start time and end time of a recording session.
    | \ No newline at end of file +| Recording |
    • Configures the resolution of recording files.
    • Configures the storage address of recording files.
    • Configures the start time and end time of a recording session.
    | + +## Supported classroom types + +| Classroom type | Example | Applicable scene | Supported platform | +|---|---|---|---| +| 1-to-1 interactive teaching | One teacher provides exclusive online tutoring to one student. | Suitable for 1-to-1 personalized VIP tutoring. |
    • Web
    • Electron
    • Android
    • iOS
    | +| Small interactive online classroom | One teacher with multiple students watching and listening in real time. During the class, the teacher can invite students to *come on stage* to speak and interact with them using real-time audio and video. The class size limit is 200; contact sales@agora.io to extend it. | Suitable for teaching scenarios that emphasize peer learning, interactivity, and participation. The class size generally does not exceed 20 people. |
    • Web
    • Electron
    • Android
    • iOS
    | +| Large interactive live classroom | One teacher with multiple students watching and listening in real time. During the class, students can *raise their hands* to request to speak and interact with the teacher using real-time audio and video. The class size limit is 5,000; contact sales@agora.io to extend it. | Often used in open classes or diversion classes, where teachers deliver the lectures and assistant teachers help answer questions. |
    • Web
    • Electron
    • Android
    • iOS
    | +| Cloud classroom | The new UI style and interactive experience that are closer to education users' habits. One teacher interacting with multiple students using real-time audio and video. The class size limit is 50; contact sales@agora.io to extend it. | Suitable for small classroom teaching scenarios with strong interaction. The class size does not exceed 50 people. Having no more than 10-20 attendees is recommended. | Web | +| Online proctoring | Online proctoring refers to online monitoring of candidates' behavior through webcams, microphones, and screen sharing during online exams. The recommended online proctoring scenario is for one examiner to proctor 25 to 50 candidates at the same time. For more information, contact sales@agora.io. |
    • School exams
    • Certification exams
    • Recruitment
    |
    • Web
    • iOS
    | \ No newline at end of file diff --git a/flexible-classroom/overview/product-overview.mdx b/flexible-classroom/overview/product-overview.mdx index fea7b024a..9b2e6cd97 100644 --- a/flexible-classroom/overview/product-overview.mdx +++ b/flexible-classroom/overview/product-overview.mdx @@ -10,9 +10,9 @@ description: > Information about changes in each release of Flexible Classroom. diff --git a/flexible-classroom/reference/supported-platforms.md b/flexible-classroom/overview/supported-platforms.md similarity index 100% rename from flexible-classroom/reference/supported-platforms.md rename to flexible-classroom/overview/supported-platforms.md diff --git a/flexible-classroom/reference/technical-architecture.md b/flexible-classroom/overview/technical-architecture.md similarity index 100% rename from flexible-classroom/reference/technical-architecture.md rename to flexible-classroom/overview/technical-architecture.md diff --git a/flexible-classroom/reference/_category_.json b/flexible-classroom/reference/_category_.json index d540fd6db..e0df2f1bc 100644 --- a/flexible-classroom/reference/_category_.json +++ b/flexible-classroom/reference/_category_.json @@ -1,5 +1,5 @@ { - "position": 4, + "position": 6, "label": "Reference", "collapsible": true, "link": null diff --git a/flexible-classroom/restful-api/_category_.json b/flexible-classroom/restful-api/_category_.json new file mode 100644 index 000000000..cfcbc2fb8 --- /dev/null +++ b/flexible-classroom/restful-api/_category_.json @@ -0,0 +1,6 @@ +{ + "position": 5, + "label": "RESTful API", + "collapsible": true, + "link": null +} diff --git a/flexible-classroom/reference/classroom-api.mdx b/flexible-classroom/restful-api/classroom-api.mdx similarity index 91% rename from flexible-classroom/reference/classroom-api.mdx rename to flexible-classroom/restful-api/classroom-api.mdx index 2777852e0..19985da9a 100644 --- a/flexible-classroom/reference/classroom-api.mdx +++ b/flexible-classroom/restful-api/classroom-api.mdx @@ -24,12 +24,7 @@ The Content-Type of all requests is `application/json`. ### Authentication -Flexible Classroom Cloud Service uses tokens for authentication. You need to put the following information to the `x-agora-token` and `x-agora-uid` fields when sending your HTTP request: - -- The token generated at your server. -- The uid you use to generate the token. - -For details, see [Generate an RTM Token](/signaling/develop/authentication-workflow). +Flexible Classroom Cloud Service uses tokens for authentication. You need to put the corresponding information into the `Authorization: agora token=` field when sending your HTTP request. For details, see [Secure authentication with tokens](../develop/authentication-workflow). ## Classroom-related @@ -50,10 +45,10 @@ Call this method to create a classroom. After it is created, the classroom is re Pass the following parameters in the URL: -| Parameter | Type | Description | -| :--------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `region` | String | (Required) The region for connection. Flexible Classroom supports the following regions:
    • `cn`: Mainland China.
    • `ap`: Asia Pacific.
    • `eu`: Europe.
    • `na`: North America.
    | -| `appId` | String | (Required) Agora App ID. | +| Parameter | Type | Description | +| :--------- | :----- | :----------- | +| `region` | String | (Required) The region for connection. Flexible Classroom supports the following regions:
    • `cn`: Mainland China.
    • `ap`: Asia Pacific.
    • `eu`: Europe.
    • `na`: North America.
    | +| `appId` | String | (Required) Agora App ID. | | `roomUuid` | String | (Required) The classroom ID. This is the globally unique identifier of a classroom. It is also used as the channel name when a user joins an RTC or RTM channel. The string length must be less than 64 characters. The following characters are supported:
    • All lowercase English letters: a to z.
    • All uppercase English letters: A to Z.
    • The numbers 0 to 9.
    • The space character.
    • The following special characters: "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "\<", "=", ".", ">", "?", "@", "[", "]", "^", "\_", "\{", "\}", "\|", "~", ","
    | **Request body parameters** @@ -64,16 +59,16 @@ Pass in the following parameters in the request body: | :--------------------------------------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `roomType` | String | (Required) The type of the classroom. You can set the value to :
    • `0`: One-to-one classroom.
    • `2`: Lecture hall.
    • `4`: Small classroom.
    Once set, this parameter value cannot be changed. | | `roomName` | String | (Required) The name of the classroom. The maximum length of this parameter is 64 characters. | -| `roomProperties` | Object | (Optional) The properties of the classroom. | +| `roomProperties` | Object | (Optional) The properties of the classroom. It includes the name of the room, room number whiteboard plug-in configuration, chat plug-in configuration, room opening, end time, duration, configuration of the number of people who raise their hands to connect to the microphone, the maximum number of people to connect to the microphone, and the switch status configuration of the device for students to join the room by default, etc. | | `roomProperties.schedule` | Object | (Optional) The schedule of the classroom. | | `roomProperties.schedule.startTime` | Integer | (Optional) The start timestamp (ms) of the class. Once set, this parameter value cannot be changed. | -| `roomProperties.schedule.duration` | Integer | (Optional) The duration of the class (s). | -| `roomProperties.schedule.closeDelay` | Integer | (Optional) The delay of the class end time (s). | +| `roomProperties.schedule.duration` | Integer | (Optional) The duration of the class (s).The maximum value is 86,400 seconds, and it is recommended to set it according to the actual duration of the class. If you set the class duration and dragging duration, when recording is turned on, the maximum recording time `maxRecordingHour` parameter will be set based on the sum of the two and rounded up. See [Set the recording state](#set-the-recording-state).| +| `roomProperties.schedule.closeDelay` | Integer | (Optional) The delay of the class end time (s). in seconds. When the class duration ends, the class will enter the "End" state (state= 2). At this time, users can still enter and stay in the classroom normally. When the dragging time ends, the class will enter the "closed" state (state= 3) and all users will be kicked out.| | `roomProperties.processes` | Object | (Optional) The process of inviting students to go "on the stage". | | `roomProperties.processes.handsUp` | Object | (Optional) The settings of "on the stage". | | `roomProperties.processes.handsUp.maxAccept` | Integer | (Optional) The maximum number of students "on the stage". | -| `roomProperties.processes.handsUp.defaultAcceptRole` | String | (Optional) The default user on the stage. If you hope to set the student on the stage, set it as `"audience"`. If not, set is as `""` or do not set this parameter. | -| `roomProperties.flexProps` | Object | (Optional) The initial properties of the classroom. | +| `roomProperties.processes.handsUp.defaultAcceptRole` | String | (Optional) The default user on the stage. If you hope to set the student on the stage, set it as "audience". If not, set is as "" or do not set this parameter. In the cloud classroom scenario, default is "". | +| `roomProperties.flexProps` | Object | (Optional) The initial properties of the classroom.The user's backend can pass customized parameters to the room through this parameter.Users can set custom attributes for any classroom based on their own business needs. Flexible Classroom will synchronize changes in this attribute to all clients in the classroom to realize your own business expansion.| | `roomProperties.widgets` | Object | (Optional) Settings of the widgets in the classroom. | | `roomProperties.widgets.netlessBoard` | Object | (Optional) Settings of the whiteboard widget in the classroom. | | `roomProperties.widgets.netlessBoard.state` | Integer | (Optional) The state of the whiteboard widget in the classroom:
  1. `0`: Disabled.
  2. `1`: Enabled.
  3. | @@ -111,12 +106,22 @@ To create a Cloud Classroom, followings are the required parameters: #### Request example ```shell -curl -X POST 'https://api.sd-rtn.com/{region}/edu/apps/{yourAppId}/v2/rooms/test_class' \ +curl -X POST 'https://api.agora.io/{region}/edu/apps/{YourAppId}/v2/rooms/test_room' \ -H 'Content-Type: application/json;charset=UTF-8' \ -H 'Authorization: agora token={educationToken}' \ --data-raw '{ "roomName": "test_class", "roomType": 4, + "roleConfig": { // The audio and video permissions of students joining the room are turned on or off by default.(Optional) + "2": { + "limit": 50, + "defaultStream": { + "state": 1, + "videoState": 1, + "audioState": 1 + } + } + }, "roomProperties": { "schedule": { "startTime": 1655452800000, @@ -125,7 +130,20 @@ curl -X POST 'https://api.sd-rtn.com/{region}/edu/apps/{yourAppId}/v2/rooms/test }, "processes": { "handsUp": { - "maxAccept": 10 + "maxAccept": 10, //The maximum number of students "on the stage". + "defaultAcceptRole": "" //(Optional) The default user on the stage. If you hope to set the student on the stage, set it as "audience". If not, set is as "" or do not set this parameter.In cloud classroom Scenario default is "" + } + }, + //The user's backend can pass customized parameters to the room through this parameter.Users can set custom attributes for any classroom based on their own business needs. Flexible Classroom will synchronize changes in this attribute to all clients in the classroom to realize your own business expansion. + "flexProps": { + "exampleKey": "exampleValue" + }, + "widgets": {//The state of the widgets in the classroom : on or off + "netlessBoard": { + "state": 0 + }, + "easemobIM": { + "state": 1 } } } @@ -150,6 +168,137 @@ curl -X POST 'https://api.sd-rtn.com/{region}/edu/apps/{yourAppId}/v2/rooms/test } ``` +Note that you can just call the API to create a room, the server will respond with the `200` or `409`. `200` means the room is created, `409` means the room has already been created and you can join the classroom. + +### Query a classroom + +#### Description + +Returns all information about the room object. + +#### Prototype + +- Method: GET +- Endpoint: `{region}/edu/apps/{appId}/v2/rooms/{roomUuid}` + +#### Request parameters + +**URL parameters** + +Pass the following parameters in the URL: + +| Parameter | Type | Description | +| :--------- | :----- | :----------- | +| `region` | String | (Required) The region for connection. Flexible Classroom supports the following regions:
    • `cn`: Mainland China.
    • `ap`: Asia Pacific.
    • `eu`: Europe.
    • `na`: North America.
    | +| `appId` | String | (Required) Agora App ID. | +| `roomUuid` | String | (Required) The classroom ID. This is the globally unique identifier of a classroom. It is also used as the channel name when a user joins an RTC or RTM channel. The string length must be less than 64 characters. The following characters are supported:
    • All lowercase English letters: a to z.
    • All uppercase English letters: A to Z.
    • The numbers 0 to 9.
    • The space character.
    • The following special characters: "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "\<", "=", ".", ">", "?", "@", "[", "]", "^", "\_", "\{", "\}", "\|", "~", ","
    | + +#### Request example + +```shell +curl -X GET 'https://api.sd-rtn.com/{region}/edu/apps/{yourAppId}/v2/rooms/test_class' \ +-H 'Content-Type: application/json;charset=UTF-8' \ +-H 'Authorization: agora token={educationToken}' \ +``` + +#### Response parameters + +| Parameter | Type | Description | +| :-------- | :------ | :-------------------------------------------------------------------------------------------------- | +| `code` | Integer | Request status code:
    • 0: The request succeeds.
    • Non-zero: The request fails.
    | +| `msg` | String | Detailed information about the code. | +| `ts` | Number | The current Unix timestamp (in milliseconds) of the server in UTC. | +|`data` | Object | The returned object, which contains the following data:
    • `roomUuid`: String, room ID.
    • `roomName`: String, room name.
    • `createTime`: Integer, room creation timestamp.
    • `roomProperties`: Object, room attributes.
      • `roomType`: Integer, room type.
        • `0`: 1 to 1.
        • `2`: Large classroom.
        • `4`: Small classroom.
      • `schedule`: Object, lesson plan.
        • `state`: Integer, room state.
          • `0`: Not started.
          • `1`: Started.
          • `2`: Ended.
          • `3`: Closed.
        • `startTime`: Integer, starting time.
        • `endTime`: Integer, end time.
        • `closeTime`: Integer, closing time.
      • `widgets`: Object, component collection.
        • `netlessBoard`: Object, whiteboard component.
          • `extra`: Object, extended information.
            • `boardAppId`: String, whiteboard App ID.
            • `boardId`: String, whiteboard room ID.
            • `boardToken`: String, whiteboard room Token.
            • `boardRegion`: String, whiteboard area.
          • `state`: Integer, component state.
            • `0`: Integer, not activated.
            • `1`: Integer, activated.
        • `easemobIM`: Object, chat room component.
          • `extra`: Object, extended information.
            • `orgName`: String, organization name.
            • `appName`: String, app name.
            • `chatRoomId`: String, chat room ID.
            • `appKey`: String, app Key.
          • `state`: Integer, component state.
            • 0: Integer, not activated.
            • 1: Integer, activated.
    | + +#### Response example + +```json +{ + "msg": "Success", + "code": 0, + "ts": 1684231543281, + "data": { + "roomName": "jasoncai's Room", + "roomUuid": "3579768dd1e1eec8522d3ed76992afd04", + "scenario": "education", + "roleConfig": { + ... + }, + "roomProperties": { + "reward": { + ... + }, + "processes": { + "handsUp": { + ... + }, + "openCamera": { + ... + }, + "remoteControl": { + ... + }, + "waveArm": { + ... + } + }, + "im": { + "huanxin": { + ... + } + }, + "screen": { + ... + }, + "groups": { + ... + }, + "carousel": { + ... + }, + "widgets": { + "netlessBoard": { + "extra": { + ... + }, + "state": 1 + }, + "easemobIM": { + "extra": { + ... + } + } + }, + "schedule": { + "closeDelay": 600, + "duration": 1800 + }, + "webhookConfig": { + ... + }, + "record": { + ... + }, + "state": 0, + "board": { + "info": { + ... + } + }, + "roomType": 4 + }, + "roomTemplate": "edu_medium_v1", + "muteChat": {}, + "muteVideo": {}, + "muteAudio": {}, + "state": 0, + "checkState": false, + "createTime": 1683884683422 + } +} +``` + + ### Set the classroom state #### Description @@ -179,7 +328,6 @@ Pass the following parameter in the URL. ```shell curl -X PUT 'https://api.sd-rtn.com/{region}/edu/apps/{yourAppId}/v2/rooms/test_class/states/1' \ -H 'Authorization: agora token={educationToken}' \ --H 'x-agora-uid: {rtmToken}' ``` #### Response parameters @@ -589,49 +737,45 @@ curl -X PATCH 'https://api.sd-rtn.com/{region}/edu/apps/{yourAppId}/v2/rooms/tes #### Response parameters -| Parameter | Type | Description | -| :-------- | :------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `code` | Integer | Business status code:
    • 0: The request succeeds.
    • Non-zero: The request fails.
    | -| `msg` | String | The detailed information. | -| `ts` | Number | The current Unix timestamp (in milliseconds) of the server in UTC. | -| `data` | Object | Include the following parameters:
    • `count`: Integer, the number of pieces of data in this batch.
    • `list`: JSONArray. An array of the recording list. A JSON object includes the following parameters:
      • `appId`: Your Agora App ID.
      • `roomUuid`: The classroom ID. This is the globally unique identifier of a classroom. It is also used as the channel name when a user joins a channel.
      • `recordId`: The unique identifier of a recording session. A recording session starts when you call a method to start recording and ends when you call this method to stop recording.
      • `startTime`: The UTC timestamp when a recording session starts, in milliseconds.
      • `endTime`: The UTC timestamp when a recording session ends, in milliseconds.
      • `resourceId`: The `resourceId` of the Agora Cloud Recording service.
      • `sid`: The `sid` of the Agora Cloud Recording service.
      • `recordUid`: The UID used by the Agora Cloud Recording service in the channel.
      • `boardAppId`: The App Identifier of the Agora Interactive Whiteboard service.
      • `boardToken`: The SDK Token of the Agora Interactive Whiteboard service.
      • `boardId`: The unique identifier of a whiteboard session.
      • `type`: Integer, the recording type:
        • `1`: Individual Recording
        • `2`: Composite Recording
      • `status`: Integer, the recording state:
        • `1`: In recording.
        • `2`: Recording has ended.
      • `url`: String, the URL address of the recorded files in composite recording mode.
      • `recordDetails`: JSONArray. The JSON object contains the following fields:
        • `url`: String, the URL address of the recorded files in web page recording mode.
      • `nextId`: String, the starting ID of the next batch of data. If it is null, there is no next batch of data. If it is not null, use this `nextId` to continue the query until null is reported.
      • `total`: Integer, the total number of pieces of data.
      • `unready`: Boolean. `true` means that recording fails.
    • `webRecordingUrlQuery`: String. Same as `query` in webpage recording.
    | +| Parameter | Type | Description | +| :-------- | :------ | :------------ | +| `code` | Integer | Business status code:
    • 0: The request succeeds.
    • Non-zero: The request fails.
    | +| `msg` | String | The detailed information. | +| `ts` | Number | The current Unix timestamp (in milliseconds) of the server in UTC. | +| `data` | Object | Include the following parameters:
    • `count`: Integer, the number of pieces of data in this batch.
    • `list`: JSONArray. An array of the recording list. A JSON object includes the following parameters:
      • `appId`: Your Agora App ID.
      • `roomUuid`: The classroom ID. This is the globally unique identifier of a classroom. It is also used as the channel name when a user joins a channel.
      • `recordId`: The unique identifier of a recording session. A recording session starts when you call a method to start recording and ends when you call this method to stop recording.
      • `startTime`: The UTC timestamp when a recording session starts, in milliseconds.
      • `endTime`: The UTC timestamp when a recording session ends, in milliseconds.
      • `resourceId`: The `resourceId` of the Agora Cloud Recording service.
      • `sid`: The `sid` of the Agora Cloud Recording service.
      • `recordUid`: The UID used by the Agora Cloud Recording service in the channel.
      • `boardAppId`: The App Identifier of the Agora Interactive Whiteboard service.
      • `boardToken`: The SDK Token of the Agora Interactive Whiteboard service.
      • `boardId`: The unique identifier of a whiteboard session.
      • `type`: Integer, the recording type:
        • `3`: Web Page Recording
      • `status`: Integer, the recording state:
        • `1`: In recording.
        • `2`: Recording has ended.
      • `url`: String, the URL address of the recorded files in composite recording mode.
      • `recordDetails`: JSONArray. The JSON object contains the following fields:
        • `url`: String, the URL address of the recorded files in web page recording mode.
      • `nextId`: String, the starting ID of the next batch of data. If it is null, there is no next batch of data. If it is not null, use this `nextId` to continue the query until null is reported.
      • `total`: Integer, the total number of pieces of data.
      • `unready`: Boolean. `true` means that recording fails.
    • `webRecordingUrlQuery`: String. Same as `query` in webpage recording.
    | #### Response example ```json -"status": 200, -"body": { - "code": 0, - "msg": "Success", - "ts": 1610450153520, - "data": { - "total": 17, - "list": [ - { - "recordId": "xxxxxx", - "appId": "xxxxxx", - "roomUuid": "jason0", - "startTime": 1602648426497, - "endTime": 1602648430262, - "resourceId": "xxxxxx", - "sid": "xxxxxx", - "recordUid": "xxxxxx", - "boardId": "xxxxxx", - "boardToken": "xxxxxx", - "type": 2, - "status": 2, - "url": "scenario/recording/xxxxxx/xxxxxx/xxxxxx.m3u8", + "msg":"Success", + "code":0, + "ts":1706091167911, + "data":{ + "total":1, + "list":[ + { + "recordId":"sssssssssss", + "appId":"sssssssssss", + "roomUuid":"sssssssssss", + "startTime":1706079930586, + "endTime":1706081355989, + "resourceId":"443322222", + "sid":"sssssssssss", + "recordUid":"sssssssssss", + "type":3, + "status":2, + "url":"https://xxxxxxxx.m3u8", "recordDetails":[ - { - "url":"xxxx/xxxx.mp4" - } - ] - }, - {...}, + { + "url":"xxxxxxxxx.mp4" + } + ], + "webRecordUrlQuery":"xxxxxxx" + } ], - "count": 17 - } + "count":1 + } } ``` @@ -662,7 +806,6 @@ Pass the following parameter in the URL. ```shell curl -X GET 'https://api.sd-rtn.com/{region}/edu/apps/{yourAppId}/v2/rooms/test_class/users/test_user' \ -H 'Authorization: agora token={educationToken}' \ --H 'x-agora-uid: {rtmToken}' ``` #### Response parameters @@ -919,14 +1062,13 @@ Pass the following parameters in the URL: | Parameter | Type | Description | | :-------- | :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `nextId` | String | (Optional) The starting ID of the next batch of data. When you call this method to get the data for the first time, leave this parameter empty or set it as null. Afterward, you can set this parameter as the `nextId` that you get in the response of the previous method call. | -| `cmd` | Integer | (Optional) Event type. For details, see [Flexible Classroom Cloud Service Events](../reference/classroom-api#events). | +| `cmd` | Integer | (Optional) Event type. For details, see [Flexible Classroom Cloud Service Events](../restful-api/classroom-api#events). | #### Request example ```shell curl -X GET 'https://api.sd-rtn.com/{region}/edu/apps/{yourAppId}/v2/rooms/test_class/sequences?cmd=20' \ -H 'Authorization: agora token={educationToken}' \ --H 'x-agora-uid: {rtmToken}' ``` #### Response parameters @@ -936,7 +1078,7 @@ curl -X GET 'https://api.sd-rtn.com/{region}/edu/apps/{yourAppId}/v2/rooms/test_ | `code` | Integer | Business status code:
    • 0: The request succeeds.
    • Non-zero: The request fails.
    | | `msg` | String | The detailed information. | | `ts` | Number | The current Unix timestamp (in milliseconds) of the server in UTC. | -| `data` | Object | Include the following parameters:
    • `total`: Integer, the total number of pieces of data.
    • `count`: Integer, the number of pieces of data in this batch.
    • `list`: JSONArray. An array of the recording list. A JSON object includes the following parameters:
        • `roomUuid`: String, the classroom ID.
        • `cmd`: Integer, the event type. For details, see [Flexible Classroom Cloud Service Events](../reference/classroom-api#events).
        • `sequence`: Integer. The event ID. This is the unique identifier of an event, which is automatically generated to ensure the order of events.
        • `version`: Integer, the service version.
        • `data`: Object, the detailed data of the event. The data varies depending on the event type. For details, see [Flexible Classroom Cloud Service Events](../reference/classroom-api#events).
    • `nextId`: String, the starting ID of the next batch of data. If it is null, there is no next batch of data. If it is not null, use this `nextId` to continue the query until null is reported.
    | +| `data` | Object | Include the following parameters:
    • `total`: Integer, the total number of pieces of data.
    • `count`: Integer, the number of pieces of data in this batch.
    • `list`: JSONArray. An array of the recording list. A JSON object includes the following parameters:
        • `roomUuid`: String, the classroom ID.
        • `cmd`: Integer, the event type. For details, see [Flexible Classroom Cloud Service Events](../restful-api/classroom-api#events).
        • `sequence`: Integer. The event ID. This is the unique identifier of an event, which is automatically generated to ensure the order of events.
        • `version`: Integer, the service version.
        • `data`: Object, the detailed data of the event. The data varies depending on the event type. For details, see [Flexible Classroom Cloud Service Events](../reference/classroom-api#events).
    • `nextId`: String, the starting ID of the next batch of data. If it is null, there is no next batch of data. If it is not null, use this `nextId` to continue the query until null is reported.
    | #### Response example @@ -993,7 +1135,6 @@ Pass the following parameter in the URL. ```shell curl -X GET 'https://api.sd-rtn.com/{region}/edu/polling/apps/{yourAppId}/v2/rooms/sequences' \ -H 'Authorization: agora token={educationToken}' \ --H 'x-agora-uid: {rtmToken}' ``` #### Response parameters @@ -1003,7 +1144,7 @@ curl -X GET 'https://api.sd-rtn.com/{region}/edu/polling/apps/{yourAppId}/v2/roo | `code` | Integer | Business status code:
    • 0: The request succeeds.
    • Non-zero: The request fails.
    | | `msg` | String | The detailed information. | | `ts` | Number | The current Unix timestamp (in milliseconds) of the server in UTC. | -| `data` | Object | Include the following parameters:
    • `roomUuid`: String, the classroom ID.
    • `cmd`: Integer, the event type. For details, see [Flexible Classroom Cloud Service Events](../reference/classroom-api#events).
    • `sequence`: Integer. The event ID. This is the unique identifier of an event, which is automatically generated to ensure the order of events.
    • `version`: Integer, the service version.
    • `data`: Object, the detailed data of the event. The data varies depending on the event type. For details, see [Flexible Classroom Cloud Service Events](../reference/classroom-api#events).
    | +| `data` | Object | Include the following parameters:
    • `roomUuid`: String, the classroom ID.
    • `cmd`: Integer, the event type. For details, see [Flexible Classroom Cloud Service Events](../restful-api/classroom-api#events).
    • `sequence`: Integer. The event ID. This is the unique identifier of an event, which is automatically generated to ensure the order of events.
    • `version`: Integer, the service version.
    • `data`: Object, the detailed data of the event. The data varies depending on the event type. For details, see [Flexible Classroom Cloud Service Events](../reference/classroom-api#events).
    | #### Response example @@ -1057,7 +1198,6 @@ Pass the following parameters in the URL: ```shell curl -X GET 'https://api.sd-rtn.com/{region}/edu/apps/{yourAppId}/v2/rooms/test_class/widgets/popupQuiz/sequences' \ -H 'Authorization: agora token={educationToken}' \ --H 'x-agora-uid: {rtmToken}' ``` #### Response parameters @@ -1255,7 +1395,6 @@ Pass the following parameters in the URL: ```shell curl -X GET 'https://api.sd-rtn.com/{region}/edu/apps/{yourAppId}/v2/rooms/test_class/widgets/popupQuiz/sequences' \ -H 'Authorization: agora token={educationToken}' \ --H 'x-agora-uid: {rtmToken}' ``` #### Response parameters diff --git a/interactive-live-streaming/develop/media-stream-encryption.mdx b/interactive-live-streaming/develop/media-stream-encryption.mdx index 24b44b57b..430dac628 100644 --- a/interactive-live-streaming/develop/media-stream-encryption.mdx +++ b/interactive-live-streaming/develop/media-stream-encryption.mdx @@ -1,5 +1,5 @@ --- -title: 'Secure channels with encryption' +title: 'Secure channel encryption' sidebar_position: 4 type: docs description: > diff --git a/interactive-live-streaming/reference/pricing.mdx b/interactive-live-streaming/overview/pricing.mdx similarity index 95% rename from interactive-live-streaming/reference/pricing.mdx rename to interactive-live-streaming/overview/pricing.mdx index 790fffa28..77782addc 100644 --- a/interactive-live-streaming/reference/pricing.mdx +++ b/interactive-live-streaming/overview/pricing.mdx @@ -1,6 +1,6 @@ --- title: Pricing -sidebar_position: 1 +sidebar_position: 3 description: > Provides you with information on billing, fee deductions, free-of-charge policy, and any suspension to your account based on the account type. --- diff --git a/interactive-live-streaming/reference/release-notes.mdx b/interactive-live-streaming/overview/release-notes.mdx similarity index 94% rename from interactive-live-streaming/reference/release-notes.mdx rename to interactive-live-streaming/overview/release-notes.mdx index 3e6e2f902..954c36367 100644 --- a/interactive-live-streaming/reference/release-notes.mdx +++ b/interactive-live-streaming/overview/release-notes.mdx @@ -1,6 +1,6 @@ --- title: 'Release notes' -sidebar_position: 2 +sidebar_position: 4 type: docs description: > Information about changes in each release of Video Calling. diff --git a/interactive-live-streaming/reference/supported-platforms.mdx b/interactive-live-streaming/overview/supported-platforms.mdx similarity index 100% rename from interactive-live-streaming/reference/supported-platforms.mdx rename to interactive-live-streaming/overview/supported-platforms.mdx diff --git a/interactive-live-streaming/reference/error-codes.mdx b/interactive-live-streaming/reference/error-codes.mdx new file mode 100644 index 000000000..5a43fd283 --- /dev/null +++ b/interactive-live-streaming/reference/error-codes.mdx @@ -0,0 +1,13 @@ +--- +title: 'Error codes' +sidebar_position: 5 +type: docs +description: > + List of commonly encountered API errors and their causes. +--- + +import ErrorCodes from '@docs/shared/video-sdk/reference/_error-codes.mdx'; + +export const toc = [{}]; + + \ No newline at end of file diff --git a/interactive-live-streaming/reference/known-issues.mdx b/interactive-live-streaming/reference/known-issues.mdx deleted file mode 100644 index 9447e4156..000000000 --- a/interactive-live-streaming/reference/known-issues.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: 'Known issues' -sidebar_position: 2 -type: docs -description: > - Known issues and limitations of using the Web SDK. ---- - - -import KnownIssues from '@docs/shared/video-sdk/reference/_known-issues.mdx'; - -export const toc = [{}]; - - - diff --git a/interactive-live-streaming/reference/service-limits.mdx b/interactive-live-streaming/reference/service-limits.mdx new file mode 100644 index 000000000..ea3e77dc5 --- /dev/null +++ b/interactive-live-streaming/reference/service-limits.mdx @@ -0,0 +1,13 @@ +--- +title: 'Service limits' +sidebar_position: 5 +type: docs +description: > + The service limits imposed by Agora +--- + +import ServiceLimits from '@docs/shared/video-sdk/reference/_service_limits.mdx'; + +export const toc = [{}]; + + \ No newline at end of file diff --git a/interactive-whiteboard/develop/authentication-workflow.md b/interactive-whiteboard/develop/authentication-workflow.md index 38093822e..6ca606b4e 100644 --- a/interactive-whiteboard/develop/authentication-workflow.md +++ b/interactive-whiteboard/develop/authentication-workflow.md @@ -66,7 +66,7 @@ A Task Token is linked with a file-conversion task under a whiteboard project in You can generate a token for the through one of the following methods: -- Use [](https://console.agora.io/). See [Get security credentials for your whiteboard project](../develop/enable-whiteboard#get-security-credentials-for-your-whiteboard-project). +- Use . See [Get security credentials for your whiteboard project](../develop/enable-whiteboard#get-security-credentials-for-your-whiteboard-project). This method can only generate a permanent admin SDK Token. Do not send this token to your app clients; otherwise, there may be a risk of exposure. @@ -86,7 +86,7 @@ When generating a token, pass in the following parameters: The access keys consist of an AK (Access Key) and an SK (Secret Key). Follow these steps to get the access keys: -1. On the [Project Management](https://console.agora.io/projects) page in , find the whiteboard project and click **Edit**. +1. On the Project Management page in , find the whiteboard project and click **Edit**. 2. On the **Edit Project** page, find **Whiteboard** and click **Config**. 3. On the **Whiteboard Configuration** page, find **AK** and **SK**. Click the eye icons on the right to copy the **AK** and **SK**, respectively, to a secure location.![](https://web-cdn.agora.io/docs-files/1619577586541) diff --git a/interactive-whiteboard/develop/enable-whiteboard.md b/interactive-whiteboard/develop/enable-whiteboard.md index 49f6d285b..e45036510 100644 --- a/interactive-whiteboard/develop/enable-whiteboard.md +++ b/interactive-whiteboard/develop/enable-whiteboard.md @@ -6,7 +6,7 @@ description: > Enable and configure Interactive Whiteboard --- -To use , you need to enable and configure it in [](https://console.agora.io/#onboarding). +To use , you need to enable and configure it in . ## Prerequisites @@ -18,7 +18,7 @@ Before enabling the whiteboard feature, ensure that you meet the following requi Follow these steps to enable the in : -1. Log in to [](https://console.agora.io/), and click the **Project Management** icon on the left navigation panel. +1. Log in to , and click the **Project Management** icon on the left navigation panel. 2. On the **Project Management** page, click **Config** for the project for which you want to enable the . ![](https://web-cdn.agora.io/docs-files/1641971710869) @@ -40,7 +40,7 @@ Follow these steps to enable the in : To get the security credentials, do the following steps: -1. On the [Project Management](https://console.agora.io/projects) page in , find the project that has the whiteboard feature enabled, and click **Edit**. +1. On the Projects page in , find the project that has the whiteboard feature enabled, and click **Edit**. 2. On the **Edit Project** page, find **Whiteboard** and click **Config**. @@ -61,12 +61,12 @@ Unexpected exposure of the security credentials can cause severe security proble - File conversion, including **Docs to Picture** and **Docs to web**. After enabling the file conversion feature, you can call the [RESTful APIs](../reference/whiteboard-api/file-conversion) to launch a file conversion task or query the conversion progress. -Agora charges for the file-conversion feature. See [Pricing](../reference/pricing). +Agora charges for the file-conversion feature. See [Pricing](../overview/pricing). - **Screenshot**. After enabling the screenshot feature, you can call the [RESTful APIs](../reference/whiteboard-api/screenshots) to take screenshots. Follow these steps to enable one or more features and configure the storage settings: -1. Go to the [Project Management](https://console.agora.io/projects) page in , find the project that has the whiteboard feature enabled, and click **Edit**. +1. Go to the Project Management page in , find the project that has the whiteboard feature enabled, and click **Edit**. 2. On the **Edit Project** page, find **Whiteboard** and click **Config**. diff --git a/interactive-whiteboard/develop/file-conversion-overview.md b/interactive-whiteboard/develop/file-conversion-overview.md index 0a5cbf54a..b11b79e65 100644 --- a/interactive-whiteboard/develop/file-conversion-overview.md +++ b/interactive-whiteboard/develop/file-conversion-overview.md @@ -82,7 +82,7 @@ Before you launch a file-conversion task, you must upload the source file to a t Refer to the following steps: -1. Go to the [Project Management](https://console.agora.io/projects) page in Agora Console, find the project that has the whiteboard feature enabled, and click **Configure**. +1. Go to the Project Management page in Agora Console, find the project that has the whiteboard feature enabled, and click **Configure**. 2. On the **Edit Project** page, find **Whiteboard**, and click **Config**. diff --git a/interactive-whiteboard/develop/migration-guide.md b/interactive-whiteboard/develop/migration-guide.md index c63771225..68ef06428 100644 --- a/interactive-whiteboard/develop/migration-guide.md +++ b/interactive-whiteboard/develop/migration-guide.md @@ -23,7 +23,7 @@ To migrate the projects under your current Netless account to ### A Netless account only - 1. Log in to [](https://console.agora.io ) using the email address linked with your Netless account. + 1. Log in to using the email address linked with your Netless account. 2. Click **Send Email**. @@ -31,7 +31,7 @@ To migrate the projects under your current Netless account to The reset process automatically creates a new Agora account using the email address linked with your Netless account. Then you can use the following steps to migrate your projects: - 1. Log in to [](https://console.agora.io ) again using your new password. + 1. Log in to again using your new password. 2. Follow the on-screen instructions, then click **Migrate**. @@ -39,7 +39,7 @@ The reset process automatically creates a new Agora account using the email addr ### A Netless account and an Agora account that use the same email address - 1. Log in to [](https://console.agora.io) using the email address linked with both accounts. + 1. Log in to using the email address linked with both accounts. 2. Follow the on-screen instructions, then click **Migrate**. diff --git a/interactive-whiteboard/develop/whiteboard-tools.mdx b/interactive-whiteboard/develop/whiteboard-tools.mdx index f26deb2f1..5fcdfd55c 100644 --- a/interactive-whiteboard/develop/whiteboard-tools.mdx +++ b/interactive-whiteboard/develop/whiteboard-tools.mdx @@ -166,10 +166,6 @@ Save the changes, and refresh the `index.html` page. The following toolbar displ ## Reference -### Whiteboard tool UI component - -The toolbar in the preceding example is relatively simple. Agora provides an open-source sample project on GitHub ([whiteboard-demo](https://github.com/netless-io/whiteboard-demo)), in which [@netless/tool-box](https://github.com/netless-io/whiteboard-demo/tree/master/packages/tool-box) implements a tool UI component with richer styles and functions. You can download the sample project to try it out or refer to the source code. - ### More whiteboard tools In addition to the basic editing tools listed in the Understand the tech section, the provides additional editing functions through the following methods: diff --git a/interactive-whiteboard/reference/pricing.md b/interactive-whiteboard/overview/pricing.md similarity index 95% rename from interactive-whiteboard/reference/pricing.md rename to interactive-whiteboard/overview/pricing.md index d98b88aae..b966f5265 100644 --- a/interactive-whiteboard/reference/pricing.md +++ b/interactive-whiteboard/overview/pricing.md @@ -1,6 +1,6 @@ --- title: Pricing -sidebar_position: 1 +sidebar_position: 3 description: > Provides you with information on billing, fee deductions, free-of-charge policy, and any suspension to your account based on the account type. --- @@ -69,7 +69,7 @@ If your scenario involves other Agora products or services, such as the begins once you enable and implement the service in your project and occurs monthly. calculates your monthly pricing by adding up the usage of each whiteboard feature in all -projects under your [Agora account](https://console.agora.io/), subtracting your monthly free usage allowances, +projects under your Agora Account, subtracting your monthly free usage allowances, multiplying each resulting usage number by the corresponding price, and adding up the cost of each feature. The basic formulas are shown here: @@ -137,7 +137,7 @@ Agora rounds up the total cost to two decimal places. You can check your usage of in . Perform the following steps: -1. Log in to [](https://console.agora.io/) and click the **Products & Usage** button on the left navigation panel. +1. Log in to and click the **Usage** button on the left navigation panel. 2. Click the arrowhead in the top left corner, and select the project you want to check in the drop-down box. @@ -152,5 +152,4 @@ You can check your usage of in . Perform the ## See also - [Billing policies and free-of-charge policy](../reference/billing-policies) -- [QPS-based Pricing](../reference/qps-pricing) diff --git a/interactive-whiteboard/reference/release-notes-uikit.mdx b/interactive-whiteboard/overview/release-notes-uikit.mdx similarity index 94% rename from interactive-whiteboard/reference/release-notes-uikit.mdx rename to interactive-whiteboard/overview/release-notes-uikit.mdx index 6037dfd26..1f2b5e09a 100644 --- a/interactive-whiteboard/reference/release-notes-uikit.mdx +++ b/interactive-whiteboard/overview/release-notes-uikit.mdx @@ -1,6 +1,6 @@ --- title: 'Release notes (Fastboard)' -sidebar_position: 4 +sidebar_position: 5 type: docs description: > Information about changes in each release of Fastboard. diff --git a/interactive-whiteboard/reference/release-notes.mdx b/interactive-whiteboard/overview/release-notes.mdx similarity index 94% rename from interactive-whiteboard/reference/release-notes.mdx rename to interactive-whiteboard/overview/release-notes.mdx index e5aff390c..cadae64f6 100644 --- a/interactive-whiteboard/reference/release-notes.mdx +++ b/interactive-whiteboard/overview/release-notes.mdx @@ -1,6 +1,6 @@ --- title: 'Release notes (Whiteboard)' -sidebar_position: 3 +sidebar_position: 4 type: docs description: > Information about changes in each release of Interactive whiteboard. diff --git a/interactive-whiteboard/reference/supported-platforms.mdx b/interactive-whiteboard/overview/supported-platforms.mdx similarity index 93% rename from interactive-whiteboard/reference/supported-platforms.mdx rename to interactive-whiteboard/overview/supported-platforms.mdx index 4bc2ca949..9426b2ea0 100644 --- a/interactive-whiteboard/reference/supported-platforms.mdx +++ b/interactive-whiteboard/overview/supported-platforms.mdx @@ -1,6 +1,6 @@ --- title: 'Supported platforms' -sidebar_position: 9 +sidebar_position: 6 type: docs description: > The platforms supported by this product. diff --git a/interactive-whiteboard/reference/downloads.mdx b/interactive-whiteboard/reference/downloads.mdx index 0d42cbc42..a176e0a83 100644 --- a/interactive-whiteboard/reference/downloads.mdx +++ b/interactive-whiteboard/reference/downloads.mdx @@ -3,7 +3,7 @@ title: 'Samples and demos' sidebar_position: 3 type: docs description: > - Links to demos and sample code for Interactive Whiteboard and Fastboard + Links to demos and sample code for Fastboard --- import * as data from '@site/data/variables'; @@ -13,9 +13,6 @@ See the sample code and download the demo to experience the a ### Agora-provided demos and samples -- **Interactive Whiteboard**: - - Demo app for Web - - Code sample for Web - **Fastboard**: - Demo app for Web - Code sample for Web @@ -23,16 +20,12 @@ See the sample code and download the demo to experience the a -- **Interactive Whiteboard**: - - Code sample for Android - **Fastboard**: - Code sample for Android -- **Interactive Whiteboard**: - - Code sample for iOS - **Fastboard**: - Code sample for iOS diff --git a/interactive-whiteboard/reference/file-conversion-overview-deprecated.mdx b/interactive-whiteboard/reference/file-conversion-overview-deprecated.mdx index 0e0ecaee3..c346b181e 100644 --- a/interactive-whiteboard/reference/file-conversion-overview-deprecated.mdx +++ b/interactive-whiteboard/reference/file-conversion-overview-deprecated.mdx @@ -64,7 +64,7 @@ You need to use an [Amazon S3](https://aws.amazon.com/s3/?nc2=h_m1) cloud storag To enable the file-conversion feature, do the following steps: -1. Go to the [Project Management](https://console.agora.io/projects) page in Agora Console, find the project that has the whiteboard feature enabled, and click **Edit**. +1. Go to the Project Management page in Agora Console, find the project that has the whiteboard feature enabled, and click **Edit**. 2. On the **Edit Project** page, find **Whiteboard**, and click **Config**. diff --git a/interactive-whiteboard/reference/uikit-sdk.mdx b/interactive-whiteboard/reference/uikit-sdk.mdx index 3477e8510..4d820d9ab 100644 --- a/interactive-whiteboard/reference/uikit-sdk.mdx +++ b/interactive-whiteboard/reference/uikit-sdk.mdx @@ -11,6 +11,4 @@ import UIKitSDK from '@docs/shared/interactive-whiteboard/uikit-sdk/index.mdx'; export const toc = [{}]; -This page provides the API reference for the . - diff --git a/interactive-whiteboard/reference/whiteboard-api/file-conversion-deprecated.mdx b/interactive-whiteboard/reference/whiteboard-api/file-conversion-deprecated.mdx index 3a85840d4..0311e9658 100644 --- a/interactive-whiteboard/reference/whiteboard-api/file-conversion-deprecated.mdx +++ b/interactive-whiteboard/reference/whiteboard-api/file-conversion-deprecated.mdx @@ -12,7 +12,7 @@ export const toc = [{}]; The file-conversion feature provided by can convert PPT, PPTX, DOC, DOCX, and PDF files into static images or dynamic HTML web pages. The generated images and web pages can be presented on the whiteboard. See [Old File Conversion Overview](/interactive-whiteboard/reference/file-conversion-overview-deprecated).
    Before calling the RESTful API for file conversion, ensure that: -
    • You have enabled Docs to Picture or Docs to Web and configured storage settings in Agora Console. See [Enable whiteboard server-side features](/interactive-whiteboard/develop/enable-whiteboard#enable-whiteboard-server-side-features).
    • You have generated a URL address for the file you want to convert, and the address is publicly accessible.
    +
    • You have enabled Docs to Picture or Docs to Web and configured storage settings in . See [Enable whiteboard server-side features](/interactive-whiteboard/develop/enable-whiteboard#enable-whiteboard-server-side-features).
    • You have generated a URL address for the file you want to convert, and the address is publicly accessible.
    ## Start file conversion (POST) diff --git a/interactive-whiteboard/reference/whiteboard-api/file-conversion.md b/interactive-whiteboard/reference/whiteboard-api/file-conversion.md index 59fe61e7d..6458767d9 100644 --- a/interactive-whiteboard/reference/whiteboard-api/file-conversion.md +++ b/interactive-whiteboard/reference/whiteboard-api/file-conversion.md @@ -10,7 +10,7 @@ description: >
    This page applies to the new version of file conversion. For the main differences between the old and new versions, see [Version comparison](/en/interactive-whiteboard/develop/file-conversion-overview#version-comparison). If you use the old file conversion, see [Old File Conversion RESTful API Reference](/en/interactive-whiteboard/reference/whiteboard-api/file-conversion-deprecated).
    -
    Before calling the RESTful API for file conversion, ensure that you have done the following:
    • You have enabled Docs to Picture or Docs to Web and configured storage settings in Agora Console. See [Enable server-side supporting features](/en/interactive-whiteboard/develop/enable-whiteboard#enable-whiteboard-server-side-features).
    • You have generated a URL address for the file you want to convert, and the address is publicly accessible.
    +
    Before calling the RESTful API for file conversion, ensure that you have done the following:
    • You have enabled Docs to Picture or Docs to Web and configured storage settings in . See [Enable server-side supporting features](/en/interactive-whiteboard/develop/enable-whiteboard#enable-whiteboard-server-side-features).
    • You have generated a URL address for the file you want to convert, and the address is publicly accessible.
    ## Start file conversion diff --git a/iot/develop/media-stream-encryption.mdx b/iot/develop/media-stream-encryption.mdx index 19e8ec08a..9cdc267d0 100644 --- a/iot/develop/media-stream-encryption.mdx +++ b/iot/develop/media-stream-encryption.mdx @@ -1,5 +1,5 @@ --- -title: 'Secure channels with encryption' +title: 'Secure channel encryption' sidebar_position: 8 type: docs description: > diff --git a/iot/reference/pricing.mdx b/iot/overview/pricing.mdx similarity index 94% rename from iot/reference/pricing.mdx rename to iot/overview/pricing.mdx index 93de702d2..780e84bec 100644 --- a/iot/reference/pricing.mdx +++ b/iot/overview/pricing.mdx @@ -1,6 +1,6 @@ --- title: Pricing -sidebar_position: 1 +sidebar_position: 3 description: > Provides you with information on billing, fee deductions, free-of-charge policy, and any suspension to your account based on the account type. --- diff --git a/iot/reference/release-notes.mdx b/iot/overview/release-notes.mdx similarity index 93% rename from iot/reference/release-notes.mdx rename to iot/overview/release-notes.mdx index 6499b2af4..fb910cdb3 100644 --- a/iot/reference/release-notes.mdx +++ b/iot/overview/release-notes.mdx @@ -1,6 +1,6 @@ --- title: 'Release notes' -sidebar_position: 1 +sidebar_position: 4 type: docs description: > Information about changes in each release of IoT SDK. diff --git a/iot/reference/supported-platforms.mdx b/iot/overview/supported-platforms.mdx similarity index 100% rename from iot/reference/supported-platforms.mdx rename to iot/overview/supported-platforms.mdx diff --git a/media-gateway/develop/_category_.json b/media-gateway/develop/_category_.json deleted file mode 100644 index bee160082..000000000 --- a/media-gateway/develop/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "position": 3, - "label": "Develop", - "collapsible": true, - "link": null -} diff --git a/media-gateway/get-started/_category_.json b/media-gateway/get-started/_category_.json deleted file mode 100644 index 8d46f797b..000000000 --- a/media-gateway/get-started/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "position": 2, - "label": "Get started", - "collapsible": true, - "link": null -} diff --git a/media-gateway/get-started/quickstart.mdx b/media-gateway/get-started/quickstart.mdx deleted file mode 100644 index 420485cc3..000000000 --- a/media-gateway/get-started/quickstart.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: 'Media Gateway quickstart' -sidebar_position: 1 -type: docs -description: > - Stream directly with RTMP/SRT protocol to Agora RTC channels. ---- - -import MEDIAGateway from '@docs/shared/media-gateway/get-started/_quickstart.mdx'; - -export const toc = [{}]; - - diff --git a/media-gateway/overview/_category_.json b/media-gateway/overview/_category_.json deleted file mode 100644 index 9874239b6..000000000 --- a/media-gateway/overview/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "position": 1, - "label": "Overview", - "collapsible": true, - "link": null -} diff --git a/media-gateway/overview/core-concepts.mdx b/media-gateway/overview/core-concepts.mdx deleted file mode 100644 index d12bcbf7a..000000000 --- a/media-gateway/overview/core-concepts.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: 'Core concepts' -sidebar_position: 2 -type: docs -platform_selector: false -description: > - Ideas that are central to developing with Agora. ---- - -import CoreConcepts from '@docs/shared/common/_core-concepts.mdx'; - -export const toc = [{}]; - - diff --git a/media-gateway/overview/product-overview.mdx b/media-gateway/overview/product-overview.mdx deleted file mode 100644 index 4fa432d77..000000000 --- a/media-gateway/overview/product-overview.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: 'Product overview' -sidebar_position: 1 -platform_selector: false -description: > - Stream directly with RTMP/SRT protocol to Agora RTC channels.. ---- - - - - allows users to directly push media streams into Agora’s Real-Time Voice and Video channels using the -RTMP/SRT protocol. To facilitate distribution, Media Gateway also allows users to perform advanced transcoding -processing on media streams. - - \ No newline at end of file diff --git a/media-gateway/reference/_category_.json b/media-gateway/reference/_category_.json deleted file mode 100644 index d540fd6db..000000000 --- a/media-gateway/reference/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "position": 4, - "label": "Reference", - "collapsible": true, - "link": null -} diff --git a/media-gateway/reference/best-practice.mdx b/media-gateway/reference/best-practice.mdx deleted file mode 100644 index ab801a37d..000000000 --- a/media-gateway/reference/best-practice.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: 'Integration best practice' -sidebar_position: 1 -type: docs -description: > - Best practice for using Media Gateway and its RESTful API. ---- - -import BestPractice from '@docs/shared/media-gateway/reference/_best-practice.mdx'; - -export const toc = [{}]; - - diff --git a/media-gateway/reference/glossary.mdx b/media-gateway/reference/glossary.mdx deleted file mode 100644 index 97b410dce..000000000 --- a/media-gateway/reference/glossary.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: 'Glossary' -sidebar_position: 5 -type: docs -platform_selector: false -description: > - A list of terms used in Agora documentation. ---- - -import Glossary from '@docs/shared/common/_glossary.mdx'; - -export const toc = [{}]; - - \ No newline at end of file diff --git a/media-gateway/reference/manage-agora-account.mdx b/media-gateway/reference/manage-agora-account.mdx deleted file mode 100644 index fddaaad75..000000000 --- a/media-gateway/reference/manage-agora-account.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: 'Agora account management' -sidebar_position: 2 -type: docs -platform_selector: false -description: > - Create, manage and update your Agora account. ---- - -import ManageAccount from '@docs/shared/common/_manage-agora-account.mdx'; - -export const toc = [{}]; - - \ No newline at end of file diff --git a/media-gateway/reference/release-notes.mdx b/media-gateway/reference/release-notes.mdx deleted file mode 100644 index d42c50347..000000000 --- a/media-gateway/reference/release-notes.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: 'Release notes' -sidebar_position: 4 -type: docs -description: > - Shows Media Gateway's past releases. -template: 'platform' ---- - -import ReleaseNotes from '@docs/shared/media-gateway/reference/_release-notes.mdx'; - -export const toc = [{}]; - - diff --git a/media-gateway/reference/security.mdx b/media-gateway/reference/security.mdx deleted file mode 100644 index 1397dd902..000000000 --- a/media-gateway/reference/security.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: 'Security' -sidebar_position: 3 -type: docs -platform_selector: false -description: > - How Agora handles security. ---- - -import Security from '@docs/shared/common/_security.mdx'; - -export const toc = [{}]; - - \ No newline at end of file diff --git a/media-pull/develop/integration-best-practices.mdx b/media-pull/develop/integration-best-practices.mdx index ecb7f0feb..e4a81f553 100644 --- a/media-pull/develop/integration-best-practices.mdx +++ b/media-pull/develop/integration-best-practices.mdx @@ -16,28 +16,55 @@ You need the following to start using RESTful API: - The scene of the channel is live, profile set to `BROADCASTING` - is enabled -- Message Notification Service is enabled to monitor events -## Limitations +Agora also recommends enabling the Message Notification Service to monitor events. -You are limited in the number of queries per second and concurrent tasks for +## Check the limits -### QPS +Check that your Peak Concurrent Worker (PCW), Queries Per Second (QPS), and the number of streams do not exceed the following limits set by Agora. -The following table shows the limits the number of queries per second (QPS) to the RESTful API. +#### QPS + +Agora sets the following QPS limits for the RESTful API: |API |QPS limit | |:--------|:---------------------| -|Create|
    • Creating cloud players with names is limited to 2 queries per second.
    • Creating cloud players without names is limited to 50 queries per second.
    | +|Create|
    • Creating Media Pull tasks (cloud players) with names is limited to 2 queries per second.
    • Creating cloud players without names is limited to 50 queries per second.
    | |Delete|Deleting cloud players is limited to 100 queries per second.| |List|
    • For a project with filter, the limit of the query rate is 2 times per second and 15 times per minute.
    • When there is no filter, the limit of query rate is 10 times per second and 20 times per minute.
    | -When the QPS is exceeded, the status code `429` (Too Many Requests) is returned. If you need a higher QPS limit, contact technical support. +When the QPS limit is exceeded, the status code `429` (Too Many Requests) is returned. To extend the QPS limit, contact support@agora.io. + +#### PCW + +The PCW limit depends on your video stream resolution and region. + +Resolutions: + +- SD: Standard definition video, resolution ≤ 640 × 360 +- HD: High definition video, resolution ≤ 1280 × 720 and > 640 × 360 +- FHD: Full HD video, resolution ≤ 1920 × 1080 and > 1280 × 720 + +| Service type | Mainland China | Europe | America | Asia (excluding mainland China) | +|:-------------|:----------------------|:----------------------|:----------------------|:--------------------------------| +| Media Pull |
    • SD 20
    • HD 20
    • FHD 10
    |
    • SD 20
    • HD 10
    • FHD 5
    |
    • SD 20
    • HD 10
    • FHD 5
    |
    • SD 20
    • HD 20
    • FHD 5
    | + +If you need to extend the PCW limit, contact support@agora.io. + +### Number of streams + +The upper limit of video attributes supported by Agora is as follows: + +- Resolution 1920 × 1080 +- Frame rate 30 FPS + +The maximum number of supported streams is as follows: -### Maximum number of concurrent tasks +| Service type | Mainland China | Europe | America | Asia (excluding mainland China) | +|:---------------|:----------------------|:---------------------|:----------------------|:--------------------------------| +| Media Pull |
    • SD 20
    • HD 20
    • FHD 10
    |
    • SD 20
    • HD 10
    • FHD 5
    |
    • SD 20
    • HD 10
    • FHD 5
    |
    • SD 20
    • HD 20
    • FHD 5
    | -The default maximum number of concurrent tasks is 50, which indicates that a maximum of 50 tasks can be run simultaneously in one project. -If a higher quota is required, contact technical support. +If you need to inject multiple streams of different resolutions at the same time, make sure you meet the following requirements:
    • The number of streams per resolution cannot exceed the corresponding limit for that resolution.
    • The total number of streams cannot exceed the limit set for the higher resolution. For example, if you need to inject both SD and HD streams in Europe, the total number of streams cannot exceed 20. If you need to inject HD and FHD, the total number cannot exceed 10.
    ## Ensure the high availability of REST services @@ -66,7 +93,7 @@ In your app, subscribe audience members to the master stream and listen to the f When you receive these notification, notify the apps where users are subscribed to the channel as audience members so that they switch to a backup stream. -When you create multiple tasks, you are charged separately for each of them. For details, see [Media Pull pricing](../reference/pricing). +When you create multiple tasks, you are charged separately for each of them. For details, see [Media Pull pricing](../overview/pricing). diff --git a/media-pull/reference/pricing.mdx b/media-pull/overview/pricing.mdx similarity index 94% rename from media-pull/reference/pricing.mdx rename to media-pull/overview/pricing.mdx index 1d9dd030c..1993c706d 100644 --- a/media-pull/reference/pricing.mdx +++ b/media-pull/overview/pricing.mdx @@ -1,6 +1,6 @@ --- title: 'Pricing' -sidebar_position: 1 +sidebar_position: 2 type: docs platform_selector: false description: > diff --git a/media-pull/overview/product-overview.mdx b/media-pull/overview/product-overview.mdx index 7ce65bb8b..03d2341e5 100644 --- a/media-pull/overview/product-overview.mdx +++ b/media-pull/overview/product-overview.mdx @@ -10,7 +10,7 @@ description: > ](../../video-calling/develop/receive-notifications) (optional). -## Limitations +## Check the limits -### QPS +Check that your Peak Concurrent Worker (PCW), Queries Per Second (QPS), and the number of streams do not exceed the following limits set by Agora. -The Agora server limits the number of queries per second (QPS) to the Media Push RESTful API. -When the QPS is exceeded, the status code `429` (Too Many Requests) -is returned. If you need a higher QPS limit, contact technical support. +#### QPS +Agora sets the following QPS limits for the Media Push RESTful API: -|API |QPS limit | -|:---------|:-----------------------------------------------------------------------| +|API |QPS limit | +|:---------|:--------------------------------------------------------| |Create |Creating a converter is limited to 50 queries per second.| |Delete |Deleting a converter is limited to 50 queries per second.| |Update |Updating a specified converter is limited to 50 queries per second.| |Get |Getting the request rate of a specified converter is limited to 50 queries per second.| -### Maximum number of concurrent tasks +When the QPS limit is exceeded, the status code `429` (Too Many Requests) +is returned. To extend the QPS limit, contact support@agora.io. -The default maximum number of concurrent tasks is 20, which indicates -that a maximum of 20 Media Push tasks can be run simultaneously in one -project. If a higher quota is required, contact technical support. +#### PCW + +The PCW limit depends on your video stream resolution and region. + +Resolutions: + +- SD: Standard definition video, resolution ≤ 640 × 360 +- HD: High definition video, resolution ≤ 1280 × 720 and > 640 × 360 +- FHD: Full HD video, resolution ≤ 1920 × 1080 and > 1280 × 720 + +| Item | Mainland China | Europe | America | Asia (excluding mainland China) | +|:-------------|:----------------------|:----------------------|:----------------------|:--------------------------------| +| RESTful API |
    • SD 300
    • HD 50
    • FHD 20
    |
    • SD 20
    • HD 5
    • FHD - Contact support@agora.io to enable
    |
    • SD 20
    • HD 5
    • FHD - Contact support@agora.io to enable
    |
    • SD 20
    • HD 5
    • FHD - Contact support@agora.io to enable
    | + +To learn the details of the PCW limits for SDK API, or if you need to extend the RESTful API PCW limit, contact support@agora.io. + +#### Number of streams + +The upper limit of video attributes supported by Agora is as follows: + +- Resolution 1920 × 1080 +- Frame rate 30 FPS + +The maximum number of supported streams is as follows: + +| Service type | Mainland China | Europe | America | Asia (excluding mainland China) | +|:-------------|:----------------------|:----------------------|:----------------------|:--------------------------------| +| Media Push |
    • SD 300
    • HD 50
    • FHD 20
    |
    • SD 20
    • HD 5
    • FHD - Contact support@agora.io to enable
    |
    • SD 20
    • HD 5
    • FHD - Contact support@agora.io to enable
    |
    • SD 20
    • HD 5
    • FHD - Contact support@agora.io to enable
    | + +If you need to upload multiple streams of different resolutions at the same time, make sure you meet the following requirements:
    • The number of streams per resolution cannot exceed the corresponding limit for that resolution.
    • The total number of streams cannot exceed the limit set for the higher resolution. For example, if you upload both SD and HD streams in America, the total number of streams cannot exceed 20. If you upload HD and FHD, the total number cannot exceed 5.
    ## Ensure the high availability of REST services @@ -53,7 +80,7 @@ After a failure is confirmed, the Media Push task is migrated within 120 seconds Consider whether you can accept the impact of high availability migration based on your own business characteristics, and decide whether to adopt higher quality assurance measures. For example, create multiple Media Push tasks for critical scenes. Alternatively, you can make periodic API calls and monitor notifications to get the latest task status, then create a new task with a different UID once you confirm the task status is unhealthy. -### Multiple Media Push tasks +### Multiple Media Push tasks (RTMP Converters) If you need a more reliable solution than fault recovery, you can use a multiple Media Push task strategy. You can choose the following two ways to implement it: @@ -78,7 +105,7 @@ For use cases with higher quality requirements, requesting multiple Media Push t -## Create RTMP Converter +## Create a Media Push RTMP Converter When creating a converter with a `create` call, pay attention to the following: @@ -88,13 +115,23 @@ When creating a converter with a `create` call, pay attention to the following: - Agora recommends that you assign a string value to the `X-Request-ID` field in the request header. The Agora server returns an `X-Custom-request-ID` field in the response header for troubleshooting purposes. - Set UID or account as the user name for the converter. Do not set these two fields at the same time. Ensure that each converter has a unique user name within the channel. - To avoid repeated media streaming due to repeated creation of multiple converters, it is recommended to use the name field to manage converters under a specific project. At the same time, converters with the same names cannot exist in a project. If you attempt to create a converter with the same name, you receive the `409` (Conflict) status code. -- Agora recommends that you assign the name using a combination of the channel name and a converter property. For example, `show68_horizontal` and `show68_vertical` would represent a horizontal and a vertical layout respectively. -- `audioOption` and `videoOptions` are set as follows for audio only or video only scenarios: +- Agora recommends that you assign the name using a combination of the channel name and a converter property. For example, `show68_horizontal` and `show68_vertical` would represent a horizontal and a vertical layout respectively. +- `audioOption` and `videoOptions` are set as follows for audio only or video only scenarios: - In a video only scenario, you do not need to set `audioOptions` and its related fields. - - In a audio only scenario, you do not need to set `videoOptions` and their related fields. + - In an audio-only scenario, you do not need to set `videoOptions` and its related fields by default. For special cases, see [Output SEI in audio-only scenarios](#output-sei-in-audio-only-scenarios). - In audio plus video scenarios, `videoOptions` and `audioOptions` are required and cannot be left blank. If you do not need to configure `audioOptions` set it to `null`. -- Set an appropriate value for `idleTimeout`. The default value of 300 seconds is recommended. It means that the converter is automatically destroyed, 300 seconds after all subscribers leave the channel. +- Set an appropriate value for `idleTimeout`. The default value of 300 seconds is recommended. It means that the converter is automatically destroyed, 300 seconds after all subscribers leave the channel. + +### Output SEI in audio-only scenarios + +In an audio-only scenario, by default, you do not need to set `videoOptions` and its related fields. However, if you need to carry additional user information, such as volume, you can set SEI information with the `seiOptions` field in `videoOptions`. + +If you want to output SEI information in Metadata or DataStream type carried by the user in an audio-only scenario and avoid video transcoding fees, follow these steps: + +1. Pass the SEI information to be output in `videoOptions.seiOptions`. +1. Ensure that the `width` and `height` in `videoOptions.canvas` are set to `16` to avoid video transcoding fees. +1. Pass the UID of the user who carries the SEI information in `rtcStreamUid` field in `videoOptions.layout`. ## Update RTMP converter diff --git a/media-push/reference/pricing.mdx b/media-push/overview/pricing.mdx similarity index 94% rename from media-push/reference/pricing.mdx rename to media-push/overview/pricing.mdx index 68e1ded6e..8d4d2b896 100644 --- a/media-push/reference/pricing.mdx +++ b/media-push/overview/pricing.mdx @@ -1,6 +1,6 @@ --- title: 'Pricing' -sidebar_position: 1 +sidebar_position: 2 type: docs platform_selector: false description: > diff --git a/media-push/overview/product-overview.mdx b/media-push/overview/product-overview.mdx index a7646b79a..8a32cbe8b 100644 --- a/media-push/overview/product-overview.mdx +++ b/media-push/overview/product-overview.mdx @@ -10,7 +10,7 @@ description: > Pricing information for On-Premise Recording. @@ -14,7 +14,7 @@ Starting in April 2021, Agora further divided HD+ video into Full HD, 2K, and 2K ## Overview -Agora calculates the billing of all projects under your [Agora account](https://console.agora.io/) monthly. +Agora calculates the billing of all projects under your Agora Account monthly. Billing for the on-premise recording service begins once you use the Agora On-Premise Recording SDK to record and save audio calls, group video calls, or interactive live video streaming made via the Agora on your server. diff --git a/on-premise-recording/reference/release-notes.mdx b/on-premise-recording/overview/release-notes.mdx similarity index 99% rename from on-premise-recording/reference/release-notes.mdx rename to on-premise-recording/overview/release-notes.mdx index 9c928c013..49e5b0aaf 100644 --- a/on-premise-recording/reference/release-notes.mdx +++ b/on-premise-recording/overview/release-notes.mdx @@ -1,6 +1,6 @@ --- -title: 'Release Notes' -sidebar_position: 2 +title: 'Release notes' +sidebar_position: 3 type: docs description: > The release notes for On-Premise Recording. diff --git a/on-premise-recording/reference/sunset.md b/on-premise-recording/reference/sunset.md index 4aa0b8d7c..39bb4c0f6 100644 --- a/on-premise-recording/reference/sunset.md +++ b/on-premise-recording/reference/sunset.md @@ -25,6 +25,6 @@ If you are using the On-Premise Recording SDK earlier than v3.0.0, upgrade as so The latest versions of the On-Premise Recording SDK have made significant improvements to user experience, service reliability, and security. To avoid service disruptions, upgrade the On-Premise Recording SDK that you are using as soon as possible by referring to the following information: - [SDK download links](https://docs.agora.io/en/Recording/downloads) -- [Release notes](../reference/release-notes) +- [Release notes](../overview/release-notes) If you encounter any problems, contact Agora for support. \ No newline at end of file diff --git a/on-premise-recording/reference/video-profile.md b/on-premise-recording/reference/video-profile.md index 96f51e80c..c5c6ae042 100644 --- a/on-premise-recording/reference/video-profile.md +++ b/on-premise-recording/reference/video-profile.md @@ -12,7 +12,7 @@ In composite recording mode, you can set the video profile (resolution, frame ra ## Basic guidelines -- Agora recommends setting the recording resolution lower than the [aggregate resolution](../reference/billing#aggregate-video-resolution) of the original video streams, otherwise the recorded video may be blurry. +- Agora recommends setting the recording resolution lower than the [aggregate resolution](../overview/billing#aggregate-video-resolution) of the original video streams, otherwise the recorded video may be blurry. - The resolution you set in the video profile is that of the video canvas, and its aspect ratio does not need to be identical to any source video stream. The aspect ratio of each user region in the output video depends on the aspect ratio of the canvas and the video layout. See [Related articles](#relateddocs). - Agora only supports the following frame rates: 1 fps, 7 fps, 10 fps, 15 fps, 24 fps, 30 fps, and 60 fps. The default value is 15 fps. If you set other frame rates, the SDK uses the default value. - The base bitrate in the video profile table applies to the communication profile. The live-broadcast profile generally requires a higher bitrate to ensure better video quality. Set the bitrate of the live-broadcast profile as twice the base bitrate. diff --git a/real-time-transcription/get-started/_category_.json b/real-time-transcription/get-started/_category_.json deleted file mode 100644 index 8d46f797b..000000000 --- a/real-time-transcription/get-started/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "position": 2, - "label": "Get started", - "collapsible": true, - "link": null -} diff --git a/real-time-transcription/get-started/get-started.mdx b/real-time-transcription/get-started/get-started.mdx deleted file mode 100644 index 7ae2ea907..000000000 --- a/real-time-transcription/get-started/get-started.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: 'Quickstart' -sidebar_position: 1 -type: docs -description: > - Transcribe audio content of a host's media stream into written words in real time. ---- - -import RealTimeTranscription from '@docs/shared/video-sdk/develop/_real-time-transcription.mdx'; - -export const toc = [{}]; - - - diff --git a/real-time-transcription/overview/_category_.json b/real-time-transcription/overview/_category_.json deleted file mode 100644 index 9874239b6..000000000 --- a/real-time-transcription/overview/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "position": 1, - "label": "Overview", - "collapsible": true, - "link": null -} diff --git a/real-time-transcription/overview/core-concepts.mdx b/real-time-transcription/overview/core-concepts.mdx deleted file mode 100644 index f0fc2ba50..000000000 --- a/real-time-transcription/overview/core-concepts.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: 'Core concepts' -sidebar_position: 2 -type: docs -platform_selector: false -description: > - Ideas that are central to developing with Agora. ---- - -import CoreConcepts from '@docs/shared/common/_core-concepts.mdx'; - -export const toc = [{}]; - - - diff --git a/real-time-transcription/overview/product-overview.mdx b/real-time-transcription/overview/product-overview.mdx deleted file mode 100644 index b834dfd0c..000000000 --- a/real-time-transcription/overview/product-overview.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: 'Product overview' -sidebar_position: 1 -platform_selector: false -description: > - Create a better user experience with the most accurate live transcription and subtitling. ---- - - - - -Agora enables you to instantly transcribe speech to text for live audio and video. Channel-based live transcription allows you to distribute live captions to all participants in channel while only paying for the duration of a channel—not the number of users. - - - - \ No newline at end of file diff --git a/real-time-transcription/reference/glossary.mdx b/real-time-transcription/reference/glossary.mdx deleted file mode 100644 index 6af6ebb7e..000000000 --- a/real-time-transcription/reference/glossary.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: 'Glossary' -sidebar_position: 10 -type: docs -platform_selector: false -description: > - A list of terms used in Agora documentation. ---- - -import Glossary from '@docs/shared/common/_glossary.mdx'; - -export const toc = [{}]; - - \ No newline at end of file diff --git a/real-time-transcription/reference/manage-agora-account.mdx b/real-time-transcription/reference/manage-agora-account.mdx deleted file mode 100644 index c5f787a17..000000000 --- a/real-time-transcription/reference/manage-agora-account.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: 'Agora account management' -sidebar_position: 9 -type: docs -platform_selector: false -description: > - Create, manage and update your Agora account. ---- - -import ManageAccount from '@docs/shared/common/_manage-agora-account.mdx'; - -export const toc = [{}]; - - diff --git a/real-time-transcription/reference/pricing.mdx b/real-time-transcription/reference/pricing.mdx deleted file mode 100644 index e5a9df319..000000000 --- a/real-time-transcription/reference/pricing.mdx +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: 'Pricing' -sidebar_position: 1 -type: docs -platform_selector: false -description: > - Introduces the billing policy for Real-Time Transcription. ---- - -export const toc = [{}]; - -This page introduces the billing policy for the add-on provided by Agora. - -Your billing details may differ if you have signed a contract with Agora. - -## Overview - -Agora calculates the billing of all projects under your Agora account on a monthly basis. Billing begins once you -enable . - - -## Transcription fee - -The Agora streaming server charges you when transcribe the subscribed streams. Agora's free-of-charge policy for the -first 10,000 minutes does not apply to the transcription fee. - - -## Unit price for transcription - -[Real-Time Transcription](../enable-features/real-time-transcription) takes the audio content of a host's media stream -and transcribes it into written words in real time. Agora charges for the time that is enabled in a channel, -which includes transcription for the active host. Transcription is available for up to 3 hosts speaking at the same time. -If several hosts speak simultaneously, Agora also charges for the time of speaking of each additional host using -the same pricing. Also note that when you enable , creates a cloud audience member who subscribes to the audio in the channel. -The usage for this audience member is added to your bill. - -|Usage, minutes per month |Pricing, US$/1,000 minutes| -|--------------------|--------------------------| -|Above 0 | 16.99 | - -Contact sales@agora.io to get a discount. diff --git a/real-time-transcription/reference/security.mdx b/real-time-transcription/reference/security.mdx deleted file mode 100644 index 878151a0d..000000000 --- a/real-time-transcription/reference/security.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: 'Security' -sidebar_position: 11 -type: docs -platform_selector: false -description: > - How Agora handles security. ---- - -import Security from '@docs/shared/common/_security.mdx'; - -export const toc = [{}]; - - \ No newline at end of file diff --git a/server-gateway/develop/media-stream-encryption.mdx b/server-gateway/develop/media-stream-encryption.mdx index c10da4262..54aaf1a10 100644 --- a/server-gateway/develop/media-stream-encryption.mdx +++ b/server-gateway/develop/media-stream-encryption.mdx @@ -1,5 +1,5 @@ --- -title: 'Secure channels with encryption' +title: 'Secure channel encryption' sidebar_position: 3 type: docs description: > diff --git a/server-gateway/reference/pricing.mdx b/server-gateway/overview/pricing.mdx similarity index 93% rename from server-gateway/reference/pricing.mdx rename to server-gateway/overview/pricing.mdx index c94ba5a89..ae7324928 100644 --- a/server-gateway/reference/pricing.mdx +++ b/server-gateway/overview/pricing.mdx @@ -1,6 +1,6 @@ --- title: 'Pricing' -sidebar_position: 1 +sidebar_position: 2 type: docs description: > Shows the pricing policy for Cloud Gateway. diff --git a/server-gateway/overview/product-overview.mdx b/server-gateway/overview/product-overview.mdx index 7e63a642a..bfc6a06c6 100644 --- a/server-gateway/overview/product-overview.mdx +++ b/server-gateway/overview/product-overview.mdx @@ -12,7 +12,7 @@ description: > img="/images/server-gateway/server-gateway-overview.png" quickStartLink="/server-gateway/get-started/integrate-sdk" apiReferenceLink="/api-reference" - samplesLink="https://download.agora.io/sdk/release/Agora-RTC-x86_64-linux-gnu-v3.8.202.20-20220627_152601-214165.tgz" + samplesLink="https://download.agora.io/rtsasdk/release/Agora-RTC-x86_64-linux-gnu-v4.2.30-20240202_172130-292462.tgz" productFeatures={[ { title: "Support for call centers, education, and testing ", @@ -26,7 +26,7 @@ description: > }, { title: "Configurable sending/receiving processes", - content: "Send and receive media streams simultaneously or choose only to send or receive audio or video streams.", + content: "Send and receive media streams simultaneously or choose only to send or receive audio or video streams. Server Gateway also supports pushing media streams to a CDN directly.", link: "" }, { @@ -48,6 +48,6 @@ description: > > -Agora Server Gateway can be deployed on your server to transmit audio and video streams to apps developed with Agora’s Voice and Video SDKs. The Server Gateway SDK enables communication between server and app through Agora’s global network (SD-RTN). Server Gateway can be used as a server-side gateway for media processing such as real-time transcription, for first- and third-party extensions, or can act as gateway service for Agora media services such as Media Push and Media Pull. +Agora Server Gateway can be deployed on your server to transmit audio and video streams to apps developed with Agora’s Voice and Video SDKs. The Server Gateway SDK enables communication between server and app through Agora’s global network (SD-RTN). Server Gateway can be used as a server-side gateway for media processing such as speech-to-text, for first- and third-party extensions, or can act as gateway service for Agora media services such as Media Pull. - \ No newline at end of file +
    diff --git a/server-gateway/reference/release-notes.mdx b/server-gateway/overview/release-notes.mdx similarity index 94% rename from server-gateway/reference/release-notes.mdx rename to server-gateway/overview/release-notes.mdx index 9d623ba46..a49d1bff0 100644 --- a/server-gateway/reference/release-notes.mdx +++ b/server-gateway/overview/release-notes.mdx @@ -1,6 +1,6 @@ --- title: 'Release notes' -sidebar_position: 2 +sidebar_position: 3 type: docs description: > Shows Cloud Gateway's past releases. diff --git a/shared/agora-analytics/_alarm.mdx b/shared/agora-analytics/_alarm.mdx index 0f5bca610..137e44c79 100644 --- a/shared/agora-analytics/_alarm.mdx +++ b/shared/agora-analytics/_alarm.mdx @@ -16,9 +16,9 @@ Alert Notifications provides the following features: To access the Alert Notifications page, do the following: -1. Subscribe to a [pricing plan](/agora-analytics/reference/pricing) to enable the **Alert Notifications** service. +1. Subscribe to a [pricing plan](/agora-analytics/overview/pricing) to enable the **Alert Notifications** service. -2. Log in to [Agora Console](https://console.agora.io/), and click **** > **Alert Notifications** on the left navigation bar. +2. Log in to , and click **** > **Alert Notifications** on the left navigation bar. ## Alert rules @@ -207,7 +207,7 @@ Alert information is sent to you through HTTP POST methods in JSON format. The f If the alert granularity is set as **Channel**: -``` json +```json { "alertTime":"1631703720000", // The timestamp (seconds) when the alert is sent "timeZone":"UTC+8", // The timezone you set for the rule @@ -224,7 +224,7 @@ If the alert granularity is set as **Channel**: If the alert granularity is set as **User**: -``` json +```json { "alertTime":"1631780400000", // The timestamp (seconds) when the alert is sent "timeZone":"UTC+8", // The timezone you set for the rule @@ -242,7 +242,7 @@ If the alert granularity is set as **User**: **Event alert** -``` json +```json { "alertTime":"1631785450000", // The timestamp (seconds) when the alert is sent "timeZone":"UTC+8", // The timezone you set for the rule diff --git a/shared/agora-analytics/_api.mdx b/shared/agora-analytics/_api.mdx index 8cd85b3e9..0b1d0339b 100644 --- a/shared/agora-analytics/_api.mdx +++ b/shared/agora-analytics/_api.mdx @@ -1,4 +1,4 @@ -Before working with the RESTful APIs, review the features in [Agora Console](https://console.agora.io) to gain a visual understanding of the quality and usage metrics that are available. For details, see the following user guides: +Before working with the RESTful APIs, review the features in to gain a visual understanding of the quality and usage metrics that are available. For details, see the following user guides: - [Call Inspector](#call-inspector) @@ -7,7 +7,7 @@ Before working with the RESTful APIs, review the features in [Ag - [Real-time Monitoring](#real-time-monitoring) -
    To use Agora Analytics RESTful APIs, subscribe to an Agora Analytics pricing plan.
    +
    To use Agora Analytics RESTful APIs, subscribe to an Agora Analytics pricing plan.
    ## Authentication @@ -26,7 +26,7 @@ With the Call Inspector RESTful APIs, you can search for calls with quality issu ### API limits -The limits of the Call Inspector RESTful APIs depend on the [pricing plan](/agora-analytics/reference/pricing) you subscribe to. +The limits of the Call Inspector RESTful APIs depend on the [pricing plan](/agora-analytics/overview/pricing) you subscribe to. The Starter, Standard, Premium, and Enterprise pricing plans have the following differences in terms of API limits: @@ -280,7 +280,7 @@ With the Data Insights RESTful APIs, you can query the usage and quality metrics ### API limits -The limits of the Data Insights RESTful APIs depend on the [pricing plan](/agora-analytics/reference/pricing) you subscribe to. +The limits of the Data Insights RESTful APIs depend on the [pricing plan](/agora-analytics/overview/pricing) you subscribe to. The Starter, Standard, Premium, and Enterprise pricing plans have the following differences in terms of API limits: @@ -781,7 +781,7 @@ The data is returned in regular 20-second time windows starting from 00:00:00. F ### API limits -The limits of the Real-time Monitoring RESTful APIs depend on the [pricing plan](/agora-analytics/reference/pricing) you subscribe to. +The limits of the Real-time Monitoring RESTful APIs depend on the [pricing plan](/agora-analytics/overview/pricing) you subscribe to. The Starter, Standard, Premium, and Enterprise pricing plans have the following differences in terms of API limits: diff --git a/shared/agora-analytics/_call-search.mdx b/shared/agora-analytics/_call-search.mdx index c42f805f7..f91a31f2d 100644 --- a/shared/agora-analytics/_call-search.mdx +++ b/shared/agora-analytics/_call-search.mdx @@ -24,7 +24,7 @@ The following workflow shows how to use the Call Inspector features together: ### Enable Call Inspector -To enable Call Inspector, subscribe to an pricing plan. For details, see [Pricing](../../reference/pricing). +To enable Call Inspector, subscribe to an pricing plan. For details, see [Pricing](../../overview/pricing). ## Use Call Search @@ -34,7 +34,7 @@ On the Call Search page, you can apply multiple filters (including channel name, To search calls, follow these steps: -1. Log in to [Agora Console](https://console.agora.io/) and click **** > **Call Inspector** on the left navigation menu. +1. Log in to and click **** > **Call Inspector** on the left navigation menu. 2. In the upper-left corner, select a project. You see all the available calls under the project. 3. Start a search through one of the following ways: @@ -42,7 +42,7 @@ To search calls, follow these steps: - Advanced search: Click **Advanced** in the upper right corner, add one or more filters as needed, and then click **Search**. -① The accessible time range depends on the data retention policy for Call Inspector features in your [Pricing Plans](/agora-analytics/reference/pricing). +① The accessible time range depends on the data retention policy for Call Inspector features in your [Pricing Plans](/agora-analytics/overview/pricing). @@ -52,7 +52,7 @@ The Call Overview page is designed to help you quickly understand the overall si To enter the Call Overview page, follow these steps: -1. Subscribe to the Premium or Enterprise pricing plan. See [Subscribe to a plan](/agora-analytics/reference/pricing#subscribe-to-a-plan). Other pricing plans do not provide access to the Call Overview page. +1. Subscribe to the Premium or Enterprise pricing plan. See [Subscribe to a plan](/agora-analytics/overview/pricing#subscribe-to-a-plan). Other pricing plans do not provide access to the Call Overview page. 2. [Use Call Search](#search) to find the call you want to inspect, then click **Call Details** in the **Action** column. - If [the number of Accumulated Call Users (ACU)](/agora-analytics/reference/call-search-terms#acu-accumulated-call-users) is greater than or equal to 50, you enter the Call Overview page. - If the number of ACU is less than 50, you first enter the Call Details page. Click the **Call Overview** tab on the top to switch. diff --git a/shared/agora-analytics/_data-insight-plus.mdx b/shared/agora-analytics/_data-insight-plus.mdx index 3b6dc82e0..7974b38e1 100644 --- a/shared/agora-analytics/_data-insight-plus.mdx +++ b/shared/agora-analytics/_data-insight-plus.mdx @@ -5,7 +5,7 @@ The regular version of Data Studio currently supports query analysis of time ser queries. offers additional capabilities, including multi-dimensional cross analysis, sampling analysis, comparative analysis, and extended querying of service indicators for , , , and . For a detailed comparison of -Standard and Premium, see [pricing](../../reference/pricing). +Standard and Premium, see [pricing](../../overview/pricing). **Want to try out this functionality? There is a 30 day trial period just for you. Please [submit a ticket](https://agora-ticket.agora.io/) to enroll for the trial.** diff --git a/shared/agora-analytics/_data-insight.mdx b/shared/agora-analytics/_data-insight.mdx index b6861eadc..f510d5e1c 100644 --- a/shared/agora-analytics/_data-insight.mdx +++ b/shared/agora-analytics/_data-insight.mdx @@ -7,9 +7,9 @@ import * as data from '@site/data/variables.js'; ## Getting started -1. Subscribe to a [pricing plan](/agora-analytics/reference/pricing) to enable the **Data Insights** feature for your project. +1. Subscribe to a [pricing plan](/agora-analytics/overview/pricing) to enable the **Data Insights** feature for your project. -2. Log in to [Agora Console](https://console.agora.io) and click **** on the left navigation bar. +2. Log in to and click **** on the left navigation bar. 3. Select a project in the top-left corner. diff --git a/shared/agora-analytics/_embedded.mdx b/shared/agora-analytics/_embedded.mdx index 4cf7ecd27..d02cf5d6a 100644 --- a/shared/agora-analytics/_embedded.mdx +++ b/shared/agora-analytics/_embedded.mdx @@ -6,7 +6,7 @@ To use the **Embed** option, ensure that the following requirements are met: 1. Your internal portal is secure and has a mechanism for managing user access. -2. You have subscribed to a [pricing plan](/agora-analytics/reference/pricing) that provides the **Embed** option for the page you want. +2. You have subscribed to a [pricing plan](/agora-analytics/overview/pricing) that provides the **Embed** option for the page you want. ## Getting started @@ -14,7 +14,7 @@ This section explains how to embed pages in your portal using th To access **Embedding Configuration**, do the following: -1. Log in to [Agora Console](https://console.agora.io/). +1. Log in to . 2. On the left navigation panel, click ****, then click the name of the feature page you want to embed (for example, **Call Inspector**). @@ -55,7 +55,7 @@ The **Embedding Configuration** dialog shows a code sample for Node.js: The response is in JSON format and returns the URL to the feature page you request. For example, if your request specifies `feature` as `callSearch`, the response looks like this: -``` html +```html https://analytics-lab.agora.io/analytics/call/search?token=xxxxxxxxxxxxxxxxxxxxxx ``` @@ -151,7 +151,7 @@ If you only append `token` and `cname` to the URL, the embedded page displays th For example: -``` html +```html https://analytics-lab.agora.io/api/analytics/research?token=xxxxxxxxxxxxxxxxxxxxxx&cname=xxxxxxxxxxxxxxxxxxxxxxxx&fromUid=xxxxxx&toUid=xxxxxx ``` diff --git a/shared/agora-analytics/_monitor.mdx b/shared/agora-analytics/_monitor.mdx index 65b50c5e5..029cc81bf 100644 --- a/shared/agora-analytics/_monitor.mdx +++ b/shared/agora-analytics/_monitor.mdx @@ -12,9 +12,9 @@ Real-time Monitoring provides the following features: To access the Real-time Monitoring page, do the following: -1. Subscribe to a [pricing plan](/agora-analytics/reference/pricing) to enable the **Real-time Monitoring** feature for your project. +1. Subscribe to a [pricing plan](/agora-analytics/overview/pricing) to enable the **Real-time Monitoring** feature for your project. -2. Log in to [Agora Console](https://console.agora.io/) and click **** > **Real-time Monitoring** on the left navigation bar. +2. Log in to and click **** > **Real-time Monitoring** on the left navigation bar. This section walks you through the Real-time Monitoring page and its basic features. diff --git a/shared/agora-analytics/_pricing.mdx b/shared/agora-analytics/_pricing.mdx index c58befc29..995cbc930 100644 --- a/shared/agora-analytics/_pricing.mdx +++ b/shared/agora-analytics/_pricing.mdx @@ -185,7 +185,7 @@ This section tells you how to subscribe and unsubscribe to an pr To subscribe to an pricing plan, do the following: -1. Log in to [Agora Console](https://console.agora.io/). +1. Log in to . 2. On the left navigation bar, click **** > **Pricing Plan** . @@ -225,7 +225,7 @@ Unsubscribing from a plan or switching to another plan takes effect on the first ## See also -- [Pricing for ](../../video-calling/reference/pricing) +- [Pricing for ](../../video-calling/overview/pricing) - [What are Agora’s policies on billing, fee deductions, and account suspension](../reference/billing-policies#billing-fee-deductions-and-account-suspension-policies) diff --git a/shared/broadcast-streaming-private-product/get-started/_agora-domain.mdx b/shared/broadcast-streaming-private-product/get-started/_agora-domain.mdx index 111620f74..c51f06400 100644 --- a/shared/broadcast-streaming-private-product/get-started/_agora-domain.mdx +++ b/shared/broadcast-streaming-private-product/get-started/_agora-domain.mdx @@ -15,7 +15,7 @@ Before proceeding, ensure that you meet the following requirements: Agora automatically assigns each project an App ID as a unique identifier. -To copy this App ID, find your project on the [Project Management](https://console.agora.io/projects) page in Agora Console, and click the copy icon in the App ID column. +To copy this App ID, find your project on the Project Management page in Agora Console, and click the copy icon in the App ID column. ### Get a Customer ID and Customer Secret @@ -23,7 +23,7 @@ Broadcast Streaming servers use a Customer ID/Customer Secret pair for authentic To generate a set of Customer ID and Customer Secret, do the following: -1. In [Agora Console](https://console.agora.io/), click the account name in the top right corner, and click **RESTful API** from the drop-down list to enter the **RESTful API** page. +1. In , click the account name in the top right corner, and click **RESTful API** from the drop-down list to enter the **RESTful API** page. ![1637661003647](https://web-cdn.agora.io/docs-files/1637661003647) 2. Click **Add a secret**, and click **OK**. A set of Customer ID and Customer Secret is generated. 3. Click **Download** in the **Customer Secret** column. Read the pop-up window carefully, and save the downloaded `key_and_secret.txt` file in a secure location. diff --git a/shared/broadcast-streaming-private-product/get-started/_enable-broadcast-streaming.mdx b/shared/broadcast-streaming-private-product/get-started/_enable-broadcast-streaming.mdx index 61c8db0ea..98c271c9f 100644 --- a/shared/broadcast-streaming-private-product/get-started/_enable-broadcast-streaming.mdx +++ b/shared/broadcast-streaming-private-product/get-started/_enable-broadcast-streaming.mdx @@ -14,7 +14,7 @@ Before enabling the Broadcast Streaming service, ensure that you meet the follow Follow these steps to enable the Broadcast Streaming service: -1. On the [Project Management](https://console.agora.io/projects) page in Agora Console, click **Config** for the project you want to use. +1. On the Project Management page in Agora Console, click **Config** for the project you want to use. ![](https://web-cdn.agora.io/docs-files/1641971710869) 2. Under **Real-time engagement core**, find **Fusion CDN**, and click **Enable**. diff --git a/shared/broadcast-streaming-private-product/reference/_message-notification.mdx b/shared/broadcast-streaming-private-product/reference/_message-notification.mdx index 5766fad07..325ad83df 100644 --- a/shared/broadcast-streaming-private-product/reference/_message-notification.mdx +++ b/shared/broadcast-streaming-private-product/reference/_message-notification.mdx @@ -23,9 +23,9 @@ Before enabling the Agora NCS, ensure that you meet the following requirements: ## Enable NCS -Follow these steps to enable the NCS on the [Agora Console](https://console.agora.io/): +Follow these steps to enable the NCS on the : -1. Log in to [Agora Console](https://console.agora.io/), and click the **Project Management** icon on the left navigation panel. +1. Log in to , and click the **Project Management** icon on the left navigation panel. 2. On the **Project Management** page, click **Config** for the project for which you want to enable the NCS. diff --git a/shared/broadcast-streaming/reference/_message-notification.mdx b/shared/broadcast-streaming/reference/_message-notification.mdx index c99127c11..80c7f7c38 100644 --- a/shared/broadcast-streaming/reference/_message-notification.mdx +++ b/shared/broadcast-streaming/reference/_message-notification.mdx @@ -23,19 +23,19 @@ Before enabling the Agora NCS, ensure that you meet the following requirements: ## Enable NCS -Follow these steps to enable the NCS on the [Agora Console](https://console.agora.io/): +Follow these steps to enable the NCS on the : -1. Log in to [Agora Console](https://console.agora.io/), and click the **Project Management** icon on the left navigation panel. +1. Log in to , and click **Projects** icon on the left navigation panel. -2. On the **Project Management** page, click **Config** for the project for which you want to enable the NCS. +2. On the **Projects** page, click **Config** for the project for which you want to enable the NCS. ![img](https://web-cdn.agora.io/docs-files/1641971710869) -3. Under **Real-time engagement core**, find **Notification Center Service**, and click **Config**. +3. Under **All Features**, find **Notifications**, and click **Config**. ![img](https://confluence.agoralab.co/download/attachments/752921702/NCS%20Config%201.png?version=1&modificationDate=1650359276982&api=v2) -4. On the **Notification Center Service Configuration** page, fill in the following information, and click **Save**: +4. On the **Configuration** page, fill in the following information, and click **Save**: - **Product Name**: channel event callbacks. diff --git a/shared/chat-sdk/client-api/_presence.mdx b/shared/chat-sdk/client-api/_presence.mdx index a3ae8314c..6e0df9c9c 100644 --- a/shared/chat-sdk/client-api/_presence.mdx +++ b/shared/chat-sdk/client-api/_presence.mdx @@ -74,7 +74,7 @@ Before proceeding, ensure that your environment meets the following requirements - You have initialized the Chat SDK. For details, see . - You understand the API call frequency limit as described in [Limitations](../reference/limitations). -- You have activated the presence feature in [Agora Console](http://console.agora.io/). +- You have activated the presence feature in . ## Implementation diff --git a/shared/chat-sdk/client-api/_reaction.mdx b/shared/chat-sdk/client-api/_reaction.mdx index 3a93617bc..4c9d23de0 100644 --- a/shared/chat-sdk/client-api/_reaction.mdx +++ b/shared/chat-sdk/client-api/_reaction.mdx @@ -35,7 +35,7 @@ Before proceeding, ensure that your environment meets the following requirements - The project integrates a version of the Chat SDK later than v1.0.3 and has implemented the basic real-time chat functionalities. - You understand the API call frequency limit as described in [Limitations](../reference/limitations). -
    The reaction feature is supported by all types of [Pricing Plans](../reference/pricing-plan-details) and is enabled by default once you have enabled Chat in Agora Console.
    +
    The reaction feature is supported by all types of [Pricing Plans](../reference/pricing-plan-details) and is enabled by default once you have enabled Chat in .
    ## Implementation diff --git a/shared/chat-sdk/client-api/chat-group/manage-group-member-attributes/project-implementation/web.mdx b/shared/chat-sdk/client-api/chat-group/manage-group-member-attributes/project-implementation/web.mdx index 0dc63a3eb..63f8c442e 100644 --- a/shared/chat-sdk/client-api/chat-group/manage-group-member-attributes/project-implementation/web.mdx +++ b/shared/chat-sdk/client-api/chat-group/manage-group-member-attributes/project-implementation/web.mdx @@ -6,7 +6,7 @@ Each chat group member can set their own attributes. Chat group admins/owners ca Refer to the following sample code to set a custom attribute of a group member: -```javaScript +```javascript let options = { groupId: 'groupId', userId: 'userId', @@ -28,7 +28,7 @@ Chat group members and group admins/owners can retrieve custom attributes of mul Refer to the following sample code to use the attribute key to fetch custom attributes of multiple group members: -```javaScript +```javascript let options = { groupId: 'groupId', userId: 'userId' @@ -46,7 +46,7 @@ connection.getGroupMemberAttributes(options).then((res) => { `GroupEvent` class holds callbacks that can be used to monitor the change of any key-value items. When such a change occurs, a `memberAttributesUpdate` callback will notify the Client SDK by returning chat group ID, UID, and key-value pairs of the changes. -```javaScript +```javascript case "memberAttributesUpdate": break; ``` diff --git a/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/android.mdx b/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/android.mdx index b0a3b8700..ca998eff5 100644 --- a/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/android.mdx +++ b/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/android.mdx @@ -27,8 +27,20 @@ ChatClient.getInstance().groupManager().addUsersToGroup(groupId, newmembers); // Chat group members can call inviteUser to invite users to a chat group. ChatClient.getInstance().groupManager().inviteUser(groupId, newmembers, null); -// The chat group owner and chat group admins can call removeUsersToGroup to remove group members from a chat group. +// The chat group owner and chat group admins can call removeUserFromGroup to remove a member from a chat group. ChatClient.getInstance().groupManager().removeUserFromGroup(groupId, username); +// The chat group owner and chat group admins can call asyncRemoveUsersFromGroup to remove members from a chat group. +ChatClient.getInstance().groupManager().asyncRemoveUsersFromGroup("GroupId", userList, new CallBack() { + @Override + public void onSuccess() { + + } + + @Override + public void onError(int code, String error) { + + } + }); ``` @@ -83,7 +95,7 @@ Refer to the following sample code to manage the chat group mute list: ```java // The chat group owner and admins can call muteGroupMember to add the specified member to the chat group mute list. The muted member and all the other chat group admins or owner receive the onMuteListAdded callback. -// If you pass `-1` to `duration`, members are muted permanently. +// duration: The mute duration. If you pass `-1`, members are muted permanently. ChatClient.getInstance().groupManager().muteGroupMembers(groupId, muteMembers, duration); // The chat group owner and admins can call unmuteGroupMember to remove the specified user from the chat group mute list. The unmuted member and all the other chat group admins or owner receive the onMuteListRemoved callback. @@ -98,6 +110,8 @@ ChatClient.getInstance().groupManager().fetchGroupMuteList(String groupId, int p The chat group owner and chat group admins can mute or unmute all the chat group members. Once all the members are muted, only those in the chat group allow list can send messages in the chat group. +Unlike muting a chat group member, this kind of mute does not expire automatically after a certain period and you need to call the `unmuteAllMembers` method to unmute all members in the chat group. + Refer to the following sample code to mute and unmute all the chat group members: ```java @@ -111,6 +125,8 @@ public void unmuteAllMembers(final String groupId, final ValueCallBack ca ### Manage the chat group allow list +The chat group owner and admins are added to the chat group allow list by default. + Members in the chat group allow list can send chat group messages even when the chat group owner or admin has muted all chat group members. However, if a member is already in the chat group mute list, adding this member to the allow list does not take effect. Refer to the following sample code to manage the chat group allow list: diff --git a/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/flutter.mdx b/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/flutter.mdx index 8bb896fbf..2ac0cb05f 100644 --- a/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/flutter.mdx +++ b/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/flutter.mdx @@ -15,7 +15,7 @@ try { ### Remove a member from a chat group -Only the chat group owner and admins can call `removeMembers` to remove the specified member from a chat group. Once removed from the chat group, this member receives the `ChatGroupEventHandler#onUserRemovedFromGroup` callback, while all the other members receive the `ChatGroupEventHandler#onMemberExitedFromGroup` callback. Users can join the chat group again after being removed. +Only the chat group owner and admins can call `removeMembers` to remove one or more members from a chat group. Once removed from the chat group, the member receives the `ChatGroupEventHandler#onUserRemovedFromGroup` callback, while all the other members receive the `ChatGroupEventHandler#onMemberExitedFromGroup` callback. Users can join the chat group again after being removed. The following code sample shows how to remove a member from a chat group: @@ -121,6 +121,7 @@ Only the chat group owner and admins can call `muteMembers` to add the specified The following code sample shows how to add a member to the chat group mute list: ```dart +// duration: The mute duration. If you pass `-1`, members are muted permanently. try { await ChatClient.getInstance.groupManager.muteMembers( groupId, @@ -163,12 +164,12 @@ try { } ``` -### Mute and unmute all the chat group members - -#### Mute all the chat group members +### Mute all the chat group members Only the chat group owner and admins can call `muteAllMembers` to mute all the chat group members. Once all the members are muted, only those in the chat group allow list can send messages in the chat group. +Unlike muting a chat group member, this kind of mute does not expire automatically after a certain period and you need to call the `unMuteAllMembers` method to unmute all members in the chat group. + The following sample code shows how to mute all the chat group members: ```dart @@ -197,6 +198,8 @@ try { ### Manage the chat group allow list +The chat group owner and admins are added to the chat group allow list by default. + #### Add a member to the chat group allow list Only the chat group owner and admins can call `addAllowList` to add the specified member to the chat group allow list. Members in the chat group allow list can send chat group messages even when the chat group owner or admin has muted all chat group members. However, if a member is already in the chat group mute list, adding this member to the allow list does not enable them to send messages. The mute list takes precedence. diff --git a/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/ios.mdx b/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/ios.mdx index 9421e18e7..320400c6b 100644 --- a/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/ios.mdx +++ b/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/ios.mdx @@ -11,7 +11,7 @@ After a user is invited to join a chat group, the implementation logic varies ba - If the user does not require a group invitation confirmation, the user receives the `groupInvitationDidAccept` callback. In this case, the user automatically accepts the group invitation and receives the `didJoinGroup` callback. All group members receive the `userDidJoinGroup` callback. 3. Remove chat group members from a chat group. -The chat group owner and chat group admins can remove chat group members from a chat group, whereas chat group members do not have this privilege. Once a group member is removed, this group member receives the `didLeaveGroup` callback and all the other group members receive the `userDidLeaveGroup` callback. +The chat group owner and chat group admins can remove chat group members from a chat group, whereas chat group members do not have this privilege. Once a group member is removed, the group member receives the `didLeaveGroup` callback and all the other group members receive the `userDidLeaveGroup` callback. Refer to the following sample code to add and remove a user: @@ -93,7 +93,8 @@ Refer to the following sample code to manage the chat group mute list: ```objc // The chat group owner and admins can call muteMembers to add the specified member to the chat group mute list. -// The muted member and all the other chat group admins or owner receive the groupMuteListDidUpdate callback. +// The muted member and all the other chat group admins or owner receive the groupMuteListDidUpdate callback. +// muteMilliseconds: The mute duration. If you pass `-1`, members are muted permanently. [[AgoraChatClient sharedClient].groupManager muteMembers:members muteMilliseconds:60 fromGroup:@"groupID" @@ -115,7 +116,9 @@ Refer to the following sample code to manage the chat group mute list: ### Mute and unmute all the chat group members -The chat group owner and chat group admins can mute or unmute all the chat group members. Once all the members are muted, only those in the chat group allow list can send messages in the chat group. +The chat group owner and chat group admins can mute or unmute all the chat group members. Once all the members are muted, only those in the chat group allow list can send messages in the chat group. Once a chat group member is added to the chat group mute list, they can no longer send chat group messages, not even after being added to the chat group allow list. + +As the mute does not expire in a certain period, you need to call the API of unmuting all chat group members to stop muting them. Refer to the following sample code to mute and unmute all the chat group members: @@ -134,6 +137,8 @@ Refer to the following sample code to mute and unmute all the chat group members ### Manage the chat group allow list +The chat group owner and admins are added to the chat group allow list by default. + Members in the chat group allow list can send chat group messages even when the chat group owner or admin has muted all chat group members. However, if a member is already in the chat group mute list, adding this member to the allow list does not take effect. Refer to the following sample code to manage the chat group allow list: diff --git a/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/react-native.mdx b/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/react-native.mdx index 82f7d92cb..3c0905c34 100644 --- a/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/react-native.mdx +++ b/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/react-native.mdx @@ -22,7 +22,7 @@ ChatClient.getInstance() ### Remove a member from a chat group -Only the chat group owner and admins can call `removeMembers` to remove the specified member from a chat group. Once removed from the chat group, this member receives the `ChatGroupEventListener#onUserRemoved` callback, while all the other members receive the `ChatGroupEventListener#onMemberExited` callback. Users can join the chat group again after being removed. +Only the chat group owner and admins can call `removeMembers` to remove one or more members from a chat group. Once removed from the chat group, the member receives the `ChatGroupEventListener#onUserRemoved` callback, while all the other members receive the `ChatGroupEventListener#onMemberExited` callback. Users can join the chat group again after being removed. The following code sample shows how to remove a member from a chat group: @@ -152,8 +152,9 @@ Only the chat group owner and admins can call `muteMembers` to add the specified The following code sample shows how to add a member to the chat group mute list: ```typescript +// duration: The mute duration. If you pass `-1`, members are muted permanently. ChatClient.getInstance() - .groupManager.muteMembers(groupId, members) + .groupManager.muteMembers(groupId, members, duration) .then(() => { console.log("mute members success."); }) @@ -202,6 +203,8 @@ ChatClient.getInstance() Only the chat group owner and admins can call `muteAllMembers` to mute all the chat group members. Once all the members are muted, only those in the chat group allow list can send messages in the chat group. +Unlike muting a chat group member, this kind of mute does not expire automatically after a certain period and you need to call the `unMuteAllMembers` method to unmute all members in the chat group. + The following sample code shows how to mute all the chat group members: ```typescript @@ -234,6 +237,8 @@ ChatClient.getInstance() ### Manage the chat group allow list +The chat group owner and admins are added to the chat group allow list by default. + #### Add a member to the chat group allow list Only the chat group owner and admins can call `addAllowList` to add the specified member to the chat group allow list. Members in the chat group allow list can send chat group messages even when the chat group owner or admin has muted all chat group members. However, if a member is already in the chat group mute list, adding this member to the allow list does not enable them to send messages. The mute list takes precedence. diff --git a/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/unity.mdx b/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/unity.mdx index 15f425aca..46c80ba2a 100644 --- a/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/unity.mdx +++ b/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/unity.mdx @@ -19,7 +19,7 @@ SDKClient.Instance.GroupManager.AddGroupMembers(groupId, members, new CallBack( ### Remove a member from a chat group -Only the chat group owner and admins can call `DeleteGroupMembers` to remove the specified member from a chat group. Once removed from the chat group, this member receives the `IGroupManagerDelegate#OnUserRemovedFromGroup` callback, while all the other members receive the `IGroupManagerDelegate#OnMemberExitedFromGroup` callback. Users can join the chat group again after being removed. +Only the chat group owner and admins can call `DeleteGroupMembers` to remove one or more members from a chat group. Once removed from the chat group, the member receives the `IGroupManagerDelegate#OnUserRemovedFromGroup` callback, while all the other members receive the `IGroupManagerDelegate#OnMemberExitedFromGroup` callback. Users can join the chat group again after being removed. The following code sample shows how to remove a member from a chat group: @@ -149,6 +149,7 @@ Only the chat group owner and admins can call `MuteGroupMembers` to add the spec The following code sample shows how to add a member to the chat group mute list: ```csharp +// muteMilliseconds: The mute duration. If you pass `-1`, members are muted permanently. SDKClient.Instance.GroupManager.MuteGroupMembers(groupId, members, new CallBack( onSuccess: () => { @@ -198,6 +199,8 @@ SDKClient.Instance.GroupManager.GetGroupMuteListFromServer(groupId, callback: ne Only the chat group owner and admins can call `MuteGroupAllMembers` to mute all the chat group members. Once all the members are muted, only those in the chat group allow list can send messages in the chat group. +Unlike muting a chat group member, this kind of mute does not expire automatically after a certain period and you need to call the `UnMuteGroupAllMembers` method to unmute all members in the chat group. + The following sample code shows how to mute all the chat group members: ```csharp @@ -228,6 +231,8 @@ SDKClient.Instance.GroupManager.UnMuteGroupAllMembers(groupId, new CallBack( ### Manage the chat group allow list +The chat group owner and admins are added to the chat group allow list by default. + #### Add a member to the chat group allow list Only the chat group owner and admins can call `AddGroupWhiteList` to add the specified member to the chat group allow list. Members in the chat group allow list can send chat group messages even when the chat group owner or admin has muted all chat group members. However, if a member is already in the chat group mute list, adding this member to the allow list does not enable them to send messages. The mute list takes precedence. diff --git a/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/web.mdx b/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/web.mdx index 16b2d8090..e6ab982d7 100644 --- a/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/web.mdx +++ b/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/web.mdx @@ -15,7 +15,7 @@ After the user is invited to join a group, the implementation logic varies based - If `inviteNeedConfirm` is set to false, the user is directly added to the chat group without confirming the group invitation. All group members receive the `memberPresence` callback. 3. Remove chat group members from a chat group. -The chat group owner and chat group admins can remove chat group members from a chat group, whereas chat group members do not have this privilege. Once a group member is removed, this group member receives the `removeMember` callback and all the other group members receive the `memberAbsence` callback. +The chat group owner and chat group admins can remove chat group members from a chat group, whereas chat group members do not have this privilege. Once removed from the group, the member receives the `removeMember` callback and all the other group members receive the `memberAbsence` callback. Refer to the following sample code to add and remove a user: @@ -25,14 +25,16 @@ let option = { users: ["user1", "user2"], groupId: "groupId" }; -conn.inviteUsersToGroup(option).then(res => console.log(res)) +connection.inviteUsersToGroup(option).then(res => console.log(res)) -// The chat group owner and chat group admins can call removeGroupMember to remove group members from a chat group. +// The chat group owner and chat group admins can call removeGroupMember to remove a group member from a chat group. let option = { groupId: "groupId", username: "username" }; -conn.removeGroupMember(option).then(res => console.log(res)) +connection.removeGroupMember(option).then(res => console.log(res)) +// The chat group owner and chat group admins can call removeGroupMembers to remove members from a chat group. +connection.removeGroupMembers({groupId: 'groupId', users: ['user1', 'user2']}) ``` @@ -111,7 +113,8 @@ Refer to the following sample code to manage the chat group mute list: ```javascript // The chat group owner and admins can call muteGroupMember to add the specified member to the chat group mute list. -// The muted member and all the other chat group admins or owner receive the muteMember callback. +// The muted member and all the other chat group admins or owner receive the muteMember callback. +// muteDuration: The mute duration. If you pass `-1`, members are muted permanently. let option = { groupId: "groupId", username: "user", @@ -139,6 +142,8 @@ conn.getGroupMutelist(option).then(res => console.log(res)) The chat group owner and chat group admins can mute or unmute all the chat group members. Once all the members are muted, only those in the chat group allow list can send messages in the chat group. +Unlike muting a chat group member, this kind of mute does not expire automatically after a certain period and you need to call the `enableSendGroupMsg` method to unmute all members in the chat group. + Refer to the following sample code to mute and unmute all the chat group members: ```javascript @@ -160,6 +165,8 @@ conn.enableSendGroupMsg(options).then(res => console.log(res)) ### Manage the chat group allow list +The chat group owner and admins are added to the chat group allow list by default. + Members in the chat group allow list can send chat group messages even when the chat group owner or admin has muted all chat group members. However, if a member is already in the chat group mute list, adding this member to the allow list does not take effect. Refer to the following sample code to manage the chat group allow list: diff --git a/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/windows.mdx b/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/windows.mdx index e5b7ae531..485f28e54 100644 --- a/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/windows.mdx +++ b/shared/chat-sdk/client-api/chat-group/manage-group-members/project-implementation/windows.mdx @@ -19,7 +19,7 @@ SDKClient.Instance.GroupManager.AddGroupMembers(groupId, members, new CallBack( ### Remove a member from a chat group -Only the chat group owner and admins can call `DeleteGroupMembers` to remove the specified member from a chat group. Once removed from the chat group, this member receives the `IGroupManagerDelegate#OnUserRemovedFromGroup` callback, while all the other members receive the `IGroupManagerDelegate#OnMemberExitedFromGroup` callback. Users can join the chat group again after being removed. +Only the chat group owner and admins can call `DeleteGroupMembers` to remove one or more member from a chat group. Once removed from the chat group, the member receives the `IGroupManagerDelegate#OnUserRemovedFromGroup` callback, while all the other members receive the `IGroupManagerDelegate#OnMemberExitedFromGroup` callback. Users can join the chat group again after being removed. The following code sample shows how to remove a member from a chat group: @@ -149,6 +149,7 @@ Only the chat group owner and admins can call `MuteGroupMembers` to add the spec The following code sample shows how to add a member to the chat group mute list: ```csharp +// muteMilliseconds: The mute duration. If you pass `-1`, members are muted permanently. SDKClient.Instance.GroupManager.MuteGroupMembers(groupId, members, new CallBack( onSuccess: () => { @@ -198,6 +199,8 @@ SDKClient.Instance.GroupManager.GetGroupMuteListFromServer(groupId, callback: ne Only the chat group owner and admins can call `MuteGroupAllMembers` to mute all the chat group members. Once all the members are muted, only those in the chat group allow list can send messages in the chat group. +Unlike muting a chat group member, this kind of mute does not expire automatically after a certain period and you need to call the `UnMuteGroupAllMembers` method to unmute all members in the chat group. + The following sample code shows how to mute all the chat group members: ```csharp @@ -228,6 +231,8 @@ SDKClient.Instance.GroupManager.UnMuteGroupAllMembers(groupId, new CallBack( ### Manage the chat group allow list +The chat group owner and admins are added to the chat group allow list by default. + #### Add a member to the chat group allow list Only the chat group owner and admins can call `AddGroupWhiteList` to add the specified member to the chat group allow list. Members in the chat group allow list can send chat group messages even when the chat group owner or admin has muted all chat group members. However, if a member is already in the chat group mute list, adding this member to the allow list does not enable them to send messages. The mute list takes precedence. diff --git a/shared/chat-sdk/client-api/chat-room/_manage-chatroom-members.mdx b/shared/chat-sdk/client-api/chat-room/_manage-chatroom-members.mdx index c63c7a3ea..44fccbade 100644 --- a/shared/chat-sdk/client-api/chat-room/_manage-chatroom-members.mdx +++ b/shared/chat-sdk/client-api/chat-room/_manage-chatroom-members.mdx @@ -14,11 +14,9 @@ This page shows how to use the Chat SDK to manage the members of a chat room in - Retrieve the member list of a chat room - Manage the block list of a chat room - Manage the mute list of a chat room -- Manage the chat room allow list - Mute and unmute all the chat room members - Manage the chat room allow list - Manage the owner and admins of a chat room -- Mute and unmute all the chat room members ## Prerequisites diff --git a/shared/chat-sdk/client-api/chat-room/_manage-chatrooms.mdx b/shared/chat-sdk/client-api/chat-room/_manage-chatrooms.mdx index 77ec181e7..c1dd4e211 100644 --- a/shared/chat-sdk/client-api/chat-room/_manage-chatrooms.mdx +++ b/shared/chat-sdk/client-api/chat-room/_manage-chatrooms.mdx @@ -4,7 +4,7 @@ import ProjectImplement from '@docs/shared/chat-sdk/client-api/chat-room/manage- Chat rooms enable real-time messaging among multiple users. -Chat rooms do not have a strict membership, and members do not retain any permanent relationship with each other. Once going offline, chat room members cannot receive any messages from the chat room and automatically leave the chat room after 2 minutes. Chat rooms are widely applied in live broadcast use cases such as stream chat in Twitch. +Chat rooms do not have a strict membership, and members do not retain any permanent relationship with each other. Once going offline, chat room members cannot receive any messages from the chat room and automatically leave the chat room after 2 minutes (members on the chat room allow list remain in the chat room even if they stay offline for 2 minutes or more). Chat rooms are widely applied in live broadcast use cases such as stream chat in Twitch. This page shows how to use the Chat SDK to create and manage a chat room in your app. diff --git a/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/android.mdx b/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/android.mdx index ddc7cfc95..880aaca25 100644 --- a/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/android.mdx +++ b/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/android.mdx @@ -8,7 +8,7 @@ All the chat room members can call `fetchChatRoomMembers` to retrieve the member Map members = ChatClient.getInstance().chatroomManager().fetchChatRoomMembers(chatRoomId, cursor, pageSize); ``` -The chat room owner and admin can call `removeChatRoomMembers` to remove the specified member from the chat room. Once a member is removed, this member receives the `onRemovedFromChatRoom` callback, and the other chat room members receive the `onMemberExited` callback. After being removed from a chat room, you can join this chat room again. +The chat room owner and admin can call `removeChatRoomMembers` to remove one or more members from the chat room. Once a member is removed, this member receives the `onRemovedFromChatRoom` callback, and the other chat room members receive the `onMemberExited` callback. After being removed from a chat room, you can join this chat room again. ```java ChatClient.getInstance().chatroomManager().removeChatRoomMembers(chatRoomId, members); @@ -25,7 +25,7 @@ ChatRoom chatRoom = ChatClient.getInstance().chatroomManager().blockChatroomMemb // The chat room owner or admin call unblockChatroomMembers to remove the specified user out of the block list. ChatRoom chatRoom = ChatClient.getInstance().chatroomManager().unblockChatRoomMembers(chatRoomId, members); -// The chat room owner or admin call fetchChatRoomBlackList to reveive the block list of the current chat room. +// The chat room owner or admin call fetchChatRoomBlackList to retrieve the block list of the current chat room. ChatClient.getInstance().chatroomManager().fetchChatRoomBlackList(chatRoomId, new ValueCallBack>() { @Override public void onSuccess(List value) { @@ -45,6 +45,7 @@ To manage the messages in the chat room, the chat room owner and admin can add t ```java // The chat room owner or admin call muteChatRoomMembers to add the specified user to the chat room block list. The muted member and all the other chat room admins or owner receive the onMuteListAdded callback. +// duration: The mute duration. If you pass `-1`, members are muted permanently. ChatRoom chatRoom = ChatClient.getInstance().chatroomManager().muteChatRoomMembers(chatRoomId, members, duration); // The chat room owner or admin can call unMuteChatRoomMembers to remove the specified user from the chat room block list. The unmuted member and all the other chat room admins or owner receive the onMuteListRemoved callback. @@ -58,6 +59,8 @@ Map memberMap = ChatClient.getInstance().chatroomManager().fetchC The chat room owner or admin can mute or unmute all the chat room members using `muteAllMembers`. Once all the members are muted, only those in the chat room allow list can send messages in the chat room. +Unlike muting a chat room member, this kind of mute does not expire automatically after a certain period and you need to call the `unmuteAllMembers` method to unmute all members in the chat room. + ```java // The chat room owner or admin can call muteAllMembers to mute all the chat room members. Once all the members are muted, these members receive the onAllMemberMuteStateChanged callback. ChatClient.getInstance().chatroomManager().muteAllMembers(chatRoomId, new ValueCallBack() { @@ -88,6 +91,8 @@ ChatClient.getInstance().chatroomManager().unmuteAllMembers(chatRoomId, new Valu ### Manage the chat room allow list +The chat room owner and admins are added to the chat room allow list by default. + Members in the chat room allow list can send chat room messages even when the chat room owner or admin has muted all the chat room members using `muteAllMembers`. However, if a member is already in the chat room mute list, adding this member to the allow list does not take effect. Messages sent by members in the chat room allow list are of high priority and will be delivered first, but there is no guarantee that they will be delivered. When the load is high, the server discards low-priority messages first. If the load is still high even then, the server discards high-priority messages. diff --git a/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/flutter.mdx b/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/flutter.mdx index 55e22091a..523ab8871 100644 --- a/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/flutter.mdx +++ b/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/flutter.mdx @@ -53,7 +53,7 @@ try { #### Remove a member from the chat room block list -Only the chat room owner and admins can call `unBlockChatRoomMembers` to remove the specified member from the chat room block list. +Only the chat room owner and admins can call `unBlockChatRoomMembers` to remove one or more members from the chat room block list. The following code sample shows how to remove a member from the chat room block list: @@ -95,6 +95,7 @@ Only the chat room owner and admins can call `muteChatRoomMembers` to add the sp The following code sample shows how to add a member to the chat room mute list: ```dart +// duration: The mute duration. If you pass `-1`, members are muted permanently. try { await ChatClient.getInstance.chatRoomManager.muteChatRoomMembers( roomId, @@ -142,6 +143,8 @@ try { ### Manage the chat room allow list +The chat room owner and admins are added to the chat room allow list by default. + #### Add a member to the chat room allow list Only the chat room owner and admins can call `addMembersToChatRoomAllowList` to add the specified member to the chat room allow list. Members in the chat room allow list can send chat room messages even when the chat room owner or admin has muted all chat room members. However, if a member is already in the chat room mute list, adding this member to the allow list does not enable them to send messages. The mute list takes precedence. Once added to the allow list, this member and all the other chat room admins or owner receive the `ChatRoomEventHandler#onAllowListAddedFromChatRoom` callback. @@ -193,6 +196,8 @@ try { Only the chat room owner and admins can call `muteAllChatRoomMembers` to mute all the chat room members. Once all the members are muted, the `ChatRoomEventHandler#onAllChatRoomMemberMuteStateChanged` callback is triggered and only those in the chat room allow list can send messages in the chat room. +Unlike muting a chat room member, this kind of mute does not expire automatically after a certain period and you need to call the `unMuteAllChatRoomMembers` method to unmuting all members in the chat room. + The following sample code shows how to mute all the chat room members: ```dart diff --git a/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/ios.mdx b/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/ios.mdx index 7aa41b48d..b7dfcb448 100644 --- a/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/ios.mdx +++ b/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/ios.mdx @@ -9,7 +9,7 @@ AgoraChatError *error = nil; [[AgoraChatClient sharedClient].roomManager getChatroomMemberListFromServerWithId:@"chatroomId" cursor:1 pageSize:20 error:&error]; ``` -The chat room owner and admin can call `removeMembers` to remove the specified member from the chat room. Once a member is removed, the other chat room members receive the `didDismissFromChatroom` callback. After being removed from a chat room, the chat user can join this chat room again. +The chat room owner and admin can call `removeMembers` to remove one or more members from the chat room. Once a member is removed, the other chat room members receive the `didDismissFromChatroom` callback. After being removed from a chat room, the chat user can join this chat room again. ```objc AgoraChatError *error = nil; @@ -43,6 +43,7 @@ The chat room owner and admins can add and remove the specified member from the ```objc // The chat room owner or admin can call muteMembers to add the specified user to the chat room block list. // The muted member and all the other chat room admins or owner receive the onMuteListAdded callback. +// muteMilliseconds: The mute duration. If you pass `-1`, members are muted permanently. AgoraChatError *error = nil; [[AgoraChatClient sharedClient].roomManager muteMembers:@[@"userName"] muteMilliseconds:-1 fromChatroom:@"chatroomId" error:&error]; @@ -61,6 +62,8 @@ AgoraChatError *error = nil; The chat room owner and admins can mute or unmute all the chat room members. Once all the members are muted, only those in the chat room allow list can send messages in the chat room. +As the mute does not expire in a certain period, you need to call the API of unmuting all chat group members to stop muting them. + ```objc // The chat room owner or admin can call muteAllMembersFromChatroom to mute all the chat room members. // Once all the members are muted, these members receive the chatroomMuteListDidUpdate callback. @@ -77,6 +80,8 @@ AgoraChatError *error = nil; ### Manage the chat room allow list +The chat room owner and admins are added to the chat room allow list by default. + Members in the chat room allow list can send chat room messages even when the chat room owner or an admin has muted all the chat room members. However, if a member is already in the chat room mute list, adding this member to the allow list does not take effect. ```objc diff --git a/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/react-native.mdx b/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/react-native.mdx index 1b82e1072..0bc1ea450 100644 --- a/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/react-native.mdx +++ b/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/react-native.mdx @@ -2,19 +2,22 @@ ### Remove a member from a chat room -Only the chat room owner and admins can call `removeChatRoomMembers` to remove the specified member from a chat room. Once removed from the chat room, this member receives the `onRemoved` callback, while all the other members receive the `onMemberExited` callback. Users can join the chat room again after being removed. - -The following code sample shows how to remove a member from a chat room: +Only the chat room owner and admins can call `removeChatRoomMembers` to remove one or more members from a chat room. Once removed from the chat room, this member receives the `onRemoved` callback, while all the other members receive the `onMemberExited` callback. Users can join the chat room again after being removed. +The following code sample shows how to remove members from a chat room: ```typescript -ChatClient.getInstance() - .roomManager.removeChatRoomMembers(roomId, members) - .then(() => { - console.log("remove members success."); - }) - .catch((reason) => { - console.log("remove members fail.", reason); - }); +List members = new List(); +members.Add("member1"); +members.Add("member2"); + +SDKClient.Instance.RoomManager.DeleteRoomMembers(roomId, members, new CallBack( + onSuccess: () => { + Console.WriteLine($"DeleteRoomMembers success."); + }, + onError: (code, desc) => { + Console.WriteLine($"DeleteRoomMembers failed, code:{code}, desc:{desc}"); + } +)); ``` ### Retrieve the chat room member list @@ -98,6 +101,7 @@ Only the chat room owner and admins can call `muteChatRoomMembers` to add the sp The following code sample shows how to add a member to the chat room mute list: ```typescript +// duration: The mute duration. If you pass `-1`, members are muted permanently. ChatClient.getInstance() .roomManager.muteChatRoomMembers(roomId, muteMembers, duration) .then(() => { @@ -144,6 +148,107 @@ ChatClient.getInstance() }); ``` +### Manage the chat room allow list + +The chat room owner and admins are added to the chat room allow list by default. + +Members in the chat room allow list can send chat room messages even when the chat room owner or an admin has muted all the chat room members. However, if a member is already in the chat room mute list, adding this member to the allow list does not take effect. + +#### Retrieve the allow list of the chat room + +Only the chat room owner or admin can call `fetchChatRoomAllowListFromServer` to retrieve the allow list of the current chat room. + +```typescript +ChatClient.getInstance() + .roomManager.fetchChatRoomAllowListFromServer(roomId) + .then((members) => { + console.log("get members success.", members); + }) + .catch((reason) => { + console.log("get members fail.", reason); + }); +``` + +#### Add a member to the chat room allow list + +Only the chat room owner or admin can call `addMembersToChatRoomAllowList` to add a member to the chat room allow list. The member added to the allow list, the chat room owner, and admins, except the operator, receive the `ChatRoomEventListener#onAllowListAdded` event. + +```typescript +ChatClient.getInstance() + .roomManager.addMembersToChatRoomAllowList(roomId, members) + .then((members) => { + console.log("success.", members); + }) + .catch((reason) => { + console.log("fail.", reason); + }); +``` + +#### Remove a member from the chat room allow list + +Only the chat room owner or admin can call `removeMembersFromChatRoomAllowList` to remove a member from the chat room allow list. The member removed from the allow list, the chat room owner, and admins, except the operator, receive the `ChatRoomEventListener#onAllowListRemoved` event. + +```typescript +ChatClient.getInstance() + .roomManager.removeMembersFromChatRoomAllowList(roomId, members) + .then((members) => { + console.log("success.", members); + }) + .catch((reason) => { + console.log("fail.", reason); + }); +``` + +#### Check whether the current user is in the chat room allow list + +Chat room members can call `isMemberInChatRoomAllowList` to check whether they are in the chat room allow list. + +```typescript +ChatClient.getInstance() + .roomManager.isMemberInChatRoomAllowList(roomId) + .then((members) => { + console.log("success.", members); + }) + .catch((reason) => { + console.log("fail.", reason); + }); +``` + +### Mute and unmute all the chat room members + +#### Mute all the chat room members + +Only the chat room owner and admins can call `muteAllChatRoomMembers` to mute all the chat room members. Once all the members are muted, the `onAllChatRoomMemberMuteStateChanged` callback is triggered and only those in the chat room allow list can send messages in the chat room. + +Unlike muting a chat room member, this kind of mute does not expire automatically after a certain period and you need to call the `unMuteAllChatRoomMembers` method to unmute all members in the chat room. + +The following sample code shows how to mute all the chat room members: + +```typescript +ChatClient.getInstance() + .roomManager.muteAllChatRoomMembers(roomId) + .then((members) => { + console.log("success.", members); + }) + .catch((reason) => { + console.log("fail.", reason); + }); +``` + +#### Unmute all the chat room members + +Only the chat room owner and admins can call `unMuteAllChatRoomMembers` to unmute all the chat room members. Once all the members are muted, the `onAllChatRoomMemberMuteStateChanged` callback is triggered. + +The following sample code shows how to unmute all the chat room members: + +```dart +try { + await ChatClient.getInstance.chatRoomManager.unMuteAllChatRoomMembers(); +} on ChatError catch (e) { +} +``` + + ### Manage the chat room owner and admins #### Transfer the chat room ownership diff --git a/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/unity.mdx b/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/unity.mdx index ef4b26375..db4b4f047 100644 --- a/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/unity.mdx +++ b/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/unity.mdx @@ -2,7 +2,7 @@ ### Remove a member from a chat room -Only the chat room owner and admins can call `DeleteRoomMembers` to remove the specified member from a chat room. Once removed from the chat room, this member receives the `OnRemovedFromRoom` callback, while all the other members receive the `OnMemberExitedFromRoom` callback. Users can join the chat room again after being removed. +Only the chat room owner and admins can call `DeleteRoomMembers` to remove one or more members from a chat room. Once removed from the chat room, this member receives the `OnRemovedFromRoom` callback, while all the other members receive the `OnMemberExitedFromRoom` callback. Users can join the chat room again after being removed. The following code sample shows how to remove a member from a chat room: @@ -83,6 +83,8 @@ SDKClient.Instance.RoomManager.FetchRoomBlockList(roomId, pageNum, pageSize, cal ### Manage the chat room allow list +The chat room owner and admins are added to the chat room allow list by default. + Messages sent by members in the chat room allow list are of high priority and will be delivered first, but there is no guarantee that they will be successfully delivered. When the load is high, the server discards low-priority messages first. If the load is high even then, the server also discards high-priority messages. #### Add a member to the chat room allow list @@ -140,6 +142,7 @@ Only the chat room owner and admins can call `MuteRoomMembers` to add the specif The following code sample shows how to add a member to the chat room mute list: ```csharp +// muteMilliseconds: The mute duration. If you pass `-1`, members are muted permanently. SDKClient.Instance.RoomManager.MuteRoomMembers(roomId, members, new CallBack( onSuccess: () => { }, @@ -186,6 +189,8 @@ SDKClient.Instance.RoomManager.FetchRoomMuteList(roomId, pageSize, pageNum, call Only the chat room owner and admins can call `MuteRoomMembers` to mute all the chat room members. Once all the members are muted, the `IRoomManagerDelegate#OnAllMemberMuteChangedFromChatroom` callback is triggered and only those in the chat room allow list can send messages in the chat room. +Unlike muting a chat room member, this kind of mute does not expire automatically after a certain period and you need to call the `UnMuteAllRoomMembers` method to unmute all members in the chat room. + The following sample code shows how to mute all the chat room members: ```csharp diff --git a/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/web.mdx b/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/web.mdx index 7ed309764..cb10a45e8 100644 --- a/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/web.mdx +++ b/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/web.mdx @@ -55,7 +55,7 @@ The chat room owner and admins can add and remove the specified members from the let option = { chatRoomId: "chatRoomId", // The ID of the chat room username: 'username', // The username of the muted user - muteDuration: -1000 // The mute duration. Unit: millisecond. The value of "-1000" means permanant mute. + muteDuration: -1 // muteDuration: The mute duration. If you pass `-1`, members are muted permanently. }; conn.muteChatRoomMember(option).then(res => console.log(res)) @@ -79,6 +79,8 @@ conn.getChatRoomMuteList(option).then(res => console.log(res)) The chat room owner and admins can mute or unmute all chat room members. Once all members are muted, only those in the chat room allow list can send messages in the chat room. +Unlike muting a chat room member, this kind of mute does not expire automatically after a certain period and you need to call the `enableSendChatRoomMsg` method to unmute all members in the chat room. + ```javascript // The chat room owner or admin can call disableSendChatRoomMsg to mute all the chat room members. // Once all the members are muted, these members receive the muteAllMembers callback. @@ -100,6 +102,8 @@ conn.enableSendChatRoomMsg(option).then((res) => { ### Manage the chat room allow list +The chat room owner and admins are added to the chat room allow list by default. + Members in the chat room allow list can send chat room messages even when the chat room owner or an admin has muted all the chat room members. However, if a member is already in the chat room mute list, adding this member to the allow list does not take effect. ```javascript diff --git a/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/windows.mdx b/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/windows.mdx index a73bf9dbe..6851a4b7c 100644 --- a/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/windows.mdx +++ b/shared/chat-sdk/client-api/chat-room/manage-chatroom-members/project-implementation/windows.mdx @@ -2,16 +2,21 @@ ### Remove a member from a chat room -Only the chat room owner and admins can call `DeleteRoomMembers` to remove the specified member from a chat room. Once removed from the chat room, this member receives the `OnRemovedFromRoom` callback, while all the other members receive the `OnMemberExitedFromRoom` callback. Users can join the chat room again after being removed. +Only the chat room owner and admins can call `DeleteRoomMembers` to remove one or more members from a chat room. Once removed from the chat room, the member receives the `OnRemovedFromRoom` callback, while all the other members receive the `OnMemberExitedFromRoom` callback. Users can join the chat room again after being removed. -The following code sample shows how to remove a member from a chat room: +The following code sample shows how to remove members from a chat room: ```csharp +List members = new List(); +members.Add("member1"); +members.Add("member2"); SDKClient.Instance.RoomManager.DeleteRoomMembers(roomId, members, new CallBack( - onSuccess: () => { - }, - onError: (code, desc) => { - } + onSuccess: () => { + Console.WriteLine($"DeleteRoomMembers success."); + }, + onError: (code, desc) => { + Console.WriteLine($"DeleteRoomMembers failed, code:{code}, desc:{desc}"); + } )); ``` @@ -33,6 +38,8 @@ SDKClient.Instance.RoomManager.FetchRoomMembers(roomId, cursor, pageSize, callba ### Manage the chat room block list +The chat room owner and admins are added to the chat room allow list by default. + #### Add a member to the chat room block list Only the chat room owner and admins can call `BlockRoomMembers` to add the specified member to the chat room block list. Once added to the block list, this member receives the `OnRemovedFromRoom` callback, while all the other members receive the `OnMemberExitedFromRoom` callback. After being added to block list, this user cannot send or receive messages in the chat room. They can no longer join the chat room again until they are removed from the block list. @@ -92,6 +99,7 @@ Only the chat room owner and admins can call `MuteRoomMembers` to add the specif The following code sample shows how to add a member to the chat room mute list: ```csharp +// muteMilliseconds: The mute duration. If you pass `-1`, members are muted permanently. SDKClient.Instance.RoomManager.MuteRoomMembers(roomId, members, new CallBack( onSuccess: () => { }, @@ -186,6 +194,8 @@ SDKClient.Instance.RoomManager.FetchRoomMuteList(roomId, pageSize, pageNum, call Only the chat room owner and admins can call `MuteRoomMembers` to mute all the chat room members. Once all the members are muted, the `IRoomManagerDelegate#OnAllMemberMuteChangedFromChatroom` callback is triggered and only those in the chat room allow list can send messages in the chat room. +Unlike muting a chat room member, this kind of mute does not expire automatically after a certain period and you need to call the `UnMuteAllRoomMembers` method to unmute all members in the chat room. + The following sample code shows how to mute all the chat room members: ```csharp diff --git a/shared/chat-sdk/client-api/messages/_translate-messages.mdx b/shared/chat-sdk/client-api/messages/_translate-messages.mdx index e9ec614dd..259f5d5c7 100644 --- a/shared/chat-sdk/client-api/messages/_translate-messages.mdx +++ b/shared/chat-sdk/client-api/messages/_translate-messages.mdx @@ -16,9 +16,9 @@ Before proceeding, ensure that your development environment meets the following - Your project integrates a version of the Chat SDK later than v1.0.3 and has implemented the basic real-time chat functionalities. - You understand the API call frequency limit as described in [Limitations](/agora-chat/reference/limitations). - Because this feature is enabled by the Microsoft Azure Translation API, ensure that you understand the supported target languages as described in [Language support](https://learn.microsoft.com/en-us/azure/ai-services/translator/language-support). -- Translation is not enabled by default. To use this feature, you need to subscribe to the **Pro** or **Enterprise** [pricing plan](/agora-chat/reference/pricing-plan-details) and enable it in [Agora Console](https://console.agora.io/). +- Translation is not enabled by default. To use this feature, you need to subscribe to the **Pro** or **Enterprise** [pricing plan](/agora-chat/reference/pricing-plan-details) and enable it in . -
    Add-on fees are incurred if you use this feature. See [Pricing](/agora-chat/reference/pricing#optional-add-on-fee) for details.
    +
    Add-on fees are incurred if you use this feature. See [Pricing](/agora-chat/overview/pricing#optional-add-on-fee) for details.
    ## Understand the tech The Chat SDK provides the following methods for implementing translation functionalities: diff --git a/shared/chat-sdk/client-api/messages/manage-messages/project-implementation/android.mdx b/shared/chat-sdk/client-api/messages/manage-messages/project-implementation/android.mdx index d7a437783..ef0ecba04 100644 --- a/shared/chat-sdk/client-api/messages/manage-messages/project-implementation/android.mdx +++ b/shared/chat-sdk/client-api/messages/manage-messages/project-implementation/android.mdx @@ -10,8 +10,7 @@ List conversations = ChatClient.getInstance().chatManager().getAll ### Retrieve messages in the specified conversation -Call `getAllMessages` to retrieve all the messages of this conversation in the message. Alternatively, you can call `loadMoreMsgFromDB` to load messages from the local database. The loaded message will be placed in the memory based on the timestamp of the messages. - +Call `getAllMessages` to retrieve all the messages of this conversation in the memory. Alternatively, you can call `loadMoreMsgFromDB` to load messages from the local database. The loaded message will be placed in the memory based on the timestamp of the messages. ```java Conversation conversation = ChatClient.getInstance().chatManager().getConversation(conversationId); @@ -80,6 +79,33 @@ ChatClient.getInstance().chatManager().deleteConversationFromServer(conversation }); ``` +### Delete all messages in a local conversation + +You can call `clearAllMessages` to delete all messages sent and received in a local conversation: + +```java +// conversationId: The conversation ID, which is the user ID of the peer user in one-to-one chat, group ID in group chat, and chat room ID in room chat. +Conversation conversation = ChatClient.getInstance().chatManager().getConversation(conversationId); +if(conversation != null) { + conversation.clearAllMessages(); +} +``` + +### Delete messages in a local conversation by time period + +You can call `removeMessages` to delete messages sent and received in a certain period in a local conversation. + +```java +// conversationId: The conversation ID, which is the user ID of the peer user in one-to-one chat, group ID in group chat, and chat room ID in room chat. +// startTime: The starting UNIX timestamp for message deletion. +// endTime: The end UNIX timestamp for message deletion. +Conversation conversation = ChatClient.getInstance().chatManager().getConversation(conversationId); +if(conversation != null) { + conversation.removeMessages(startTime, endTime); +} +``` + + ### Search for messages using keywords Call `searchMsgFromDB` to search for messages by keywords, timestamp, and message sender: diff --git a/shared/chat-sdk/client-api/messages/manage-messages/project-implementation/ios.mdx b/shared/chat-sdk/client-api/messages/manage-messages/project-implementation/ios.mdx index fdad54c9e..281938034 100644 --- a/shared/chat-sdk/client-api/messages/manage-messages/project-implementation/ios.mdx +++ b/shared/chat-sdk/client-api/messages/manage-messages/project-implementation/ios.mdx @@ -80,6 +80,36 @@ To delete a conversation on the server, call deleteServerConversation: }]; ``` +### Delete all messages in a local conversation + +You can call `deleteAllMessages` to delete all messages in a local conversation: + +```swift +// conversationId: The conversation ID, which is the user ID of the peer user in one-to-one chat, group ID in group chat, and chat room ID in room chat. +if let conversation = AgoraChatClient.shared().chatManager?.getConversationWithConvId("conversationId") { + var err: AgoraChatError? = nil + conversation.deleteAllMessages(&err) + if let err = err { + // Failed to delete messages + } else { + // Succeeded in deleting messages + } +} +``` + +### Delete messages in a local conversation by time period + +You can call `removeMessagesStart` to delete messages sent and received in a certain period in a local conversation. + +```swift +// conversationId: The conversation ID, which is the user ID of the peer user in one-to-one chat, group ID in group chat, and chat room ID in room chat. +// startTime: The starting UNIX timestamp for message deletion. +// endTime: The end UNIX timestamp for message deletion. +if let conversation = AgoraChatClient.shared().chatManager?.getConversationWithConvId("conversationId") { + conversation.removeMessagesStart(startTime, to: endTime) +} +``` + ### Search for messages using keywords Call `loadMessagesWithKeyword` to search for messages by keywords, timestamp, and message sender: diff --git a/shared/chat-sdk/client-api/messages/manage-messages/project-implementation/react-native.mdx b/shared/chat-sdk/client-api/messages/manage-messages/project-implementation/react-native.mdx index ce215821a..b14edb2c5 100644 --- a/shared/chat-sdk/client-api/messages/manage-messages/project-implementation/react-native.mdx +++ b/shared/chat-sdk/client-api/messages/manage-messages/project-implementation/react-native.mdx @@ -151,6 +151,40 @@ ChatClient.getInstance() }); ``` +### Delete all messages in a local conversation + +You can call `deleteAllMessages` to delete all messages sent and received in a local conversation: + +```typescript +// convId: The conversation ID. +// convType: The conversation type, which is `Chat` for one-to-one chat, `Group` for group chat, and `Room` for room chat. +ChatClient.getInstance() + .chatManager.deleteAllMessages(convId, convType) + .then(() => { + console.log("delete message success"); + }) + .catch((reason) => { + console.log("delete message fail.", reason); + }); +``` + +### Delete messages in a local conversation by time period + +You can call `deleteMessagesWithTimestamp` to delete messages sent and received in a certain period in a local conversation. + +```typescript +// startTs: The starting UNIX timestamp for message deletion. +// endTs: The end UNIX timestamp for message deletion. +ChatClient.getInstance() + .chatManager.deleteMessagesWithTimestamp({ startTs, endTs }) + .then(() => { + console.log("delete message success"); + }) + .catch((reason) => { + console.log("delete message fail.", reason); + }); +``` + ### Search for messages using keywords Call `SearchMsgFromDB` to search for messages by keywords, timestamp, and message sender: diff --git a/shared/chat-sdk/client-api/messages/manage-messages/project-implementation/windows.mdx b/shared/chat-sdk/client-api/messages/manage-messages/project-implementation/windows.mdx index c8b860bd8..b1ce72040 100644 --- a/shared/chat-sdk/client-api/messages/manage-messages/project-implementation/windows.mdx +++ b/shared/chat-sdk/client-api/messages/manage-messages/project-implementation/windows.mdx @@ -86,6 +86,40 @@ SDKClient.Instance.ChatManager.DeleteConversationFromServer(conversationId, type )); ``` +### Delete all messages in a local conversation + +You can call `DeleteAllMessages` to delete all messages sent and received in a local conversation: + +```csharp +// conversationId: The conversation ID, which is the user ID of the peer user in one-to-one chat, group ID in group chat, and chat room ID in room chat. +// conversationType: The conversation type, which is `Chat` for one-to-one chat, `Group` for group chat, and `Room` for room chat. +Conversation conv = SDKClient.Instance.ChatManager.GetConversation(conversionId, conversationType); + +if (conv.DeleteAllMessages()){ + //Succeeded in deleting messages +} +else{ + //Failed to delete messages +} +``` + +### Delete messages in a local conversation by time period + +You can call `DeleteMessages` to delete messages sent and received in a certain period in a local conversation. + +```csharp +// conversationId: The conversation ID, which is the user ID of the peer user in one-to-one chat, group ID in group chat, and chat room ID in room chat. +// conversationType: The conversation type, which is `Chat` for one-to-one chat, `Group` for group chat, and `Room` for room chat. +Conversation conv = SDKClient.Instance.ChatManager.GetConversation(conversionId, conversationType); + +if (conv.DeleteMessages(startTime, endTime)) { + //Succeeded in deleting messages +} +else { + //Failed to delete messages +} +``` + ### Search for messages using keywords Call `SearchMsgFromDB` to search for messages by keywords, timestamp, and message sender: diff --git a/shared/chat-sdk/client-api/messages/manage-messages/understand/android.mdx b/shared/chat-sdk/client-api/messages/manage-messages/understand/android.mdx index 33e3b5f56..2e669adf2 100644 --- a/shared/chat-sdk/client-api/messages/manage-messages/understand/android.mdx +++ b/shared/chat-sdk/client-api/messages/manage-messages/understand/android.mdx @@ -5,6 +5,9 @@ SQLCipher is used to encrypt the database that stores local messages. The Chat S - `getAllConversationsBySort`: Loads the conversation list on the local device. - `deleteConversation`: Deletes the specified conversation locally. - `deleteConversationFromServer`: Delete a conversation from the server. +- `removeMessage`: Deletes a message sent or received in a local conversation. +- `clearAllMessages`:Deletes all messages sent and received in a local conversation +- `removeMessages`: Deletes messages sent and received in a certain period in a local conversation. - `Conversation.getUnreadMsgCount`: Retrieves the count of unread messages in the specified conversation. - `getUnreadMessageCount`: Retrieves the count of all unread messages. - `asyncPinConversation`: Pins a conversation. diff --git a/shared/chat-sdk/client-api/messages/manage-messages/understand/flutter.mdx b/shared/chat-sdk/client-api/messages/manage-messages/understand/flutter.mdx index 64f6d09eb..570a01fd8 100644 --- a/shared/chat-sdk/client-api/messages/manage-messages/understand/flutter.mdx +++ b/shared/chat-sdk/client-api/messages/manage-messages/understand/flutter.mdx @@ -4,6 +4,9 @@ SQLCipher is used to encrypt the database that stores local messages. The Chat S - `ChatManager.loadAllConversations`: Loads the conversation list on the local device. - `ChatManage.deleteConversation`: Deletes the specified conversation. +- `ChatManager.deleteMessage`: Deletes a message sent or received in a local conversation. +- `ChatManager.deleteAllMessages`: Deletes all messages sent and received in a local conversation. +- `ChatManager.deleteMessagesWithTs`: Deletes messages sent and received in a certain period in a local conversation. - `ChatConversation.getUnreadMessageCount`: Retrieves the count of unread messages in the specified conversation. - `ChatManager.getUnreadMessageCount`: Retrieves the count of all unread messages. - `ChatManager.pinConversation`: Pins a conversation. diff --git a/shared/chat-sdk/client-api/messages/manage-messages/understand/ios.mdx b/shared/chat-sdk/client-api/messages/manage-messages/understand/ios.mdx index e74817371..542d7f346 100644 --- a/shared/chat-sdk/client-api/messages/manage-messages/understand/ios.mdx +++ b/shared/chat-sdk/client-api/messages/manage-messages/understand/ios.mdx @@ -6,6 +6,9 @@ SQLCipher is used to encrypt the database that stores local messages. The Chat S - `loadMessagesStartFromId`: Loads messages of a conversation. - `deleteConversation`: Deletes a local conversation. - `deleteConversations`: Deletes multiple local conversations. + `deleteMessage`: Deletes a message sent or received in a local conversation. +- `deleteAllMessages`: Deletes all messages in a local conversation. +- `removeMessagesStart`: Deletes messages sent and received in a certain period in a local conversation. - `AgoraChatConversation.unreadMessagesCount`: Retrieves the count of unread messages in the specified conversation. - `pinConversation`: Pins a conversation. - `getPinnedConversationsFromServerWithCursor`: Retrieves the pinned conversations from the server with pagination. diff --git a/shared/chat-sdk/client-api/messages/manage-messages/understand/react-native.mdx b/shared/chat-sdk/client-api/messages/manage-messages/understand/react-native.mdx index 90ced4664..6323ac12a 100644 --- a/shared/chat-sdk/client-api/messages/manage-messages/understand/react-native.mdx +++ b/shared/chat-sdk/client-api/messages/manage-messages/understand/react-native.mdx @@ -3,9 +3,9 @@ SQLCipher is used to encrypt the database that stores local messages. The Chat SDK uses `ChatManager` and `ChatConversation` to manage local messages. Followings are the core methods: - `ChatManager.getAllConversations`: Loads the conversation list on the local device. -- `ChatManage.deleteConversation`: Deletes the specified conversation. -- `ChatConversation.getConversationUnreadCount`: Retrieves the count of unread messages in the specified conversation. -- `ChatManager.getUnreadCount`: Retrieves the count of all unread messages. +- `ChatManager.deleteConversation`: Deletes the specified conversation. +- `ChatManager.deleteAllMessages`: Deletes all messages sent and received in a local conversation. +- `ChatManager.deleteMessagesWithTimestamp`: Deletes messages sent and received in a certain period in a local conversation. - `ChatManager.pinConversation`: Pins a conversation. - `ChatManager.getPinnedConversationsFromServerWithCursor`: Retrieves the pinned conversations from the server with pagination. - `ChatManager.removeConversationFromServer`: Deletes the conversation and historical messages from the server. diff --git a/shared/chat-sdk/client-api/messages/manage-messages/understand/unity.mdx b/shared/chat-sdk/client-api/messages/manage-messages/understand/unity.mdx index 0ef37d9a7..1a613b261 100644 --- a/shared/chat-sdk/client-api/messages/manage-messages/understand/unity.mdx +++ b/shared/chat-sdk/client-api/messages/manage-messages/understand/unity.mdx @@ -4,6 +4,8 @@ SQLCipher is used to encrypt the database that stores local messages. The Chat S - `IChatManager.LoadAllConversations`: Loads the conversation list on the local device. - `IChatManage.DeleteConversation`: Deletes the specified conversation. +- `Conversation#DeleteAllMessages`:Deletes all messages sent and received in a local conversation. +- `Conversation#DeleteMessages`: Deletes messages sent and received in a certain period in a local conversation. - `IConversationManager.UnReadCount`: Retrieves the count of unread messages in the specified conversation. - `IChatManager.GetUnreadMessageCount`: Retrieves the count of all unread messages. - `IChatManager.PinConversation`: Pins a conversation. diff --git a/shared/chat-sdk/client-api/messages/manage-messages/understand/windows.mdx b/shared/chat-sdk/client-api/messages/manage-messages/understand/windows.mdx index 4b1cb38f1..c96f5e710 100644 --- a/shared/chat-sdk/client-api/messages/manage-messages/understand/windows.mdx +++ b/shared/chat-sdk/client-api/messages/manage-messages/understand/windows.mdx @@ -4,6 +4,8 @@ SQLCipher is used to encrypt the database that stores local messages. The Chat S - `IChatManager.LoadAllConversations`: Loads the conversation list on the local device. - `IChatManage.DeleteConversation`: Deletes the specified conversation. +- `Conversation#DeleteAllMessages`:Deletes all messages sent and received in a local conversation. +- `Conversation#DeleteMessages`: Deletes messages sent and received in a certain period in a local conversation. - `IConversationManager.UnReadCount`: Retrieves the count of unread messages in the specified conversation. - `IChatManager.GetUnreadMessageCount`: Retrieves the count of all unread messages. - `IChatManager.PinConversation`: Pins a conversation. diff --git a/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/android.mdx b/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/android.mdx index 0f7f16605..0751415a9 100644 --- a/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/android.mdx +++ b/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/android.mdx @@ -7,6 +7,7 @@ The server stores 100 conversations for 7 days by default. To increase the two u ```java String cursor = ""; +limit: The number of conversations that you expect to get on each page. The value range is [1,50]. int limit = 40; List conversations = new ArrayList<>(); doAsyncFetchConversationsFromServer(limit,cursor,conversations); @@ -36,16 +37,13 @@ private void doAsyncFetchConversationsFromServer(final int limit, final String c } ``` -If you still use the `asyncFetchConversationsFromServer` method to retrieve the conversations from the server without pagination, the SDK, by default, retrieves the last ten conversations in the past seven days, and each conversation contains one last historical message. To adjust the time limit or the number of conversations retrieved, contact [support@agora.io](mailto:support@agora.io). - - ### Retrieve historical messages of the specified conversation After retrieving conversations, you can retrieve historical messages from the server. You can set the search direction to retrieve messages in the chronological or reverse chronological order of when the server receives them, the message type, the time period, the message sender, as well as whether to save the retrieved message to the local database. -If you have implemented Chat SDK after June 8, 2023, you can retrieve historical messages even before joining the Chat Group. For earlier implementations, contact [support@agora.io](mailto:support@agora.io) to enable this. +If you have integrated Chat SDK after June 8, 2023, you can retrieve historical messages even before joining the Chat Group. For earlier implementations, contact [support@agora.io](mailto:support@agora.io) to enable this. The Agora Chat server stores the full message history for a certain period of time depending on your subscribed [Chat plan](../../reference/message-overview#limitations-of-message-storage-duration). After an end user logs back into Agora Chat, the servers automatically send offline messages to them, that is, messages transmitted when that end user was offline. Offline messages are a subset of the full message history stored on Agora Chat server. Sending only a subset of messages prevents distributing too many messages to a single device, which can overwhelm it and slow down the end user login. Agora Chat server stores and manages these offline messages for every end user in the following way: @@ -160,6 +158,8 @@ private void doAsyncFetchPinnedConversationsFromServer(final int limit, final St Call `removeMessagesFromServer` to delete historical messages one way from the server. You can remove a maximum of 50 messages from the server each time. Once the messages are deleted, you can no longer retrieve them from the server. The deleted messages are automatically removed from your local device. Other chat users can still get the messages from the server. +
    To use this function, you need to contact [support@agora.io](mailto:support@agora.io) to enable it.
    + ```java Conversation conversation = ChatClient.getInstance().chatManager().getConversation(username); // Delete messages by timestamp diff --git a/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/flutter.mdx b/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/flutter.mdx index b85db9aea..1902d73ab 100644 --- a/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/flutter.mdx +++ b/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/flutter.mdx @@ -6,6 +6,7 @@ Call `fetchConversation` to retrieve conversations from the server with paginati The server stores 100 conversations for 7 days by default. To increase the two upper limits, contact support@agora.io. Agora Chat server can store up to 3,000 conversation per end user. ```dart +// pageSize: The number of conversations that you expect to get on each page. The value range is [1,50]. String? cursor; int pageSize = 10; ChatCursorResult result = @@ -15,15 +16,13 @@ ChatCursorResult result = ); ``` -For users that do not support `fetchConversation`, call `getConversationsFromServer` to retrieve the conversations from the server without pagination, the SDK, by default, retrieves the last ten conversations in the past seven days, and each conversation contains one last historical message. To adjust the time limit or the number of conversations retrieved, contact [support@agora.io](mailto:support@agora.io). - ### Retrieve historical messages of the specified conversation After retrieving conversations, you can retrieve historical messages from the server. You can set the search direction to retrieve messages in the chronological or reverse chronological order of when the server receives them, the message type, the time period, the message sender, as well as whether to save the retrieved message to the local database. -If you have implemented Chat SDK after June 8, 2023, you can retrieve historical messages even before joining the Chat Group. For earlier implementations, contact [support@agora.io](mailto:support@agora.io) to enable this. +If you have integrated Chat SDK after June 8, 2023, you can retrieve historical messages even before joining the Chat Group. For earlier implementations, contact [support@agora.io](mailto:support@agora.io) to enable this. The Agora Chat server stores the full message history for a certain period of time depending on your subscribed [Chat plan](../../reference/message-overview#limitations-of-message-storage-duration). After an end user logs back into Agora Chat, the servers automatically send offline messages to them, that is, messages transmitted when that end user was offline. Offline messages are a subset of the full message history stored on Agora Chat server. Sending only a subset of messages prevents distributing too many messages to a single device, which can overwhelm it and slow down the end user login. Agora Chat server stores and manages these offline messages for every end user in the following way: @@ -95,6 +94,8 @@ ChatCursorResult result = Call `deleteRemoteMessagesBefore` or `deleteRemoteMessagesWithIds` to delete historical messages one way from the server. You can remove a maximum of 50 messages from the server each time. Once the messages are deleted, you can no longer retrieve them from the server. The deleted messages are automatically removed from your local device. Other chat users can still get the messages from the server. +
    To use this function, you need to contact [support@agora.io](mailto:support@agora.io) to enable it.
    + ```dart try { // Delete messages by timestamp diff --git a/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/ios.mdx b/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/ios.mdx index 6f812c2aa..46d503d63 100644 --- a/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/ios.mdx +++ b/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/ios.mdx @@ -6,20 +6,19 @@ Call `getConversationsFromServerWithCursor:pageSize:completion` to retrieve conv The server stores 100 conversations for 7 days by default. To increase the two upper limits, contact support@agora.io. Agora Chat server can store up to 3,000 conversation per end user. ```objective-c +pageSize: The number of conversations that you expect to get on each page. The value range is [1,50]. [AgoraChatClient.sharedClient.chatManager getConversationsFromServerWithCursor:@"" pageSize:20 completion:^(AgoraChatCursorResult * _Nullable result, AgoraChatError * _Nullable error) { }]; ``` -For users that do not support `getConversationsFromServerWithCursor:pageSize:completion`, call `getConversationsFromServer` method to retrieve the conversations from the server, the SDK, by default, retrieves the last ten conversations in the past seven days, and each conversation contains one last historical message. To adjust the time limit or the number of conversations retrieved, contact [support@agora.io](mailto:support@agora.io). - ### Retrieve historical messages of the specified conversation After retrieving conversations, you can retrieve historical messages from the server. You can set the search direction to retrieve messages in the chronological or reverse chronological order of when the server receives them, the message type, the time period, the message sender, as well as whether to save the retrieved message to the local database. -If you have implemented Chat SDK after June 8, 2023, you can retrieve historical messages even before joining the Chat Group. For earlier implementations, contact [support@agora.io](mailto:support@agora.io) to enable this. +If you have integrated Chat SDK after June 8, 2023, you can retrieve historical messages even before joining the Chat Group. For earlier implementations, contact [support@agora.io](mailto:support@agora.io) to enable this. The Agora Chat server stores the full message history for a certain period of time depending on your subscribed [Chat plan](../../reference/message-overview#limitations-of-message-storage-duration). After an end user logs back into Agora Chat, the servers automatically send offline messages to them, that is, messages transmitted when that end user was offline. Offline messages are a subset of the full message history stored on Agora Chat server. Sending only a subset of messages prevents distributing too many messages to a single device, which can overwhelm it and slow down the end user login. Agora Chat server stores and manages these offline messages for every end user in the following way: @@ -74,6 +73,8 @@ Refer to the following code example to get a list of pinned conversations from t Call `removeMessagesFromServerWithTimeStamp` or `removeMessagesFromServerMessageIds` to delete historical messages one way from the server. You can remove a maximum of 50 messages from the server each time. Once the messages are deleted, you can no longer retrieve them from the server. The deleted messages are automatically removed from your local device. Other chat users can still get the messages from the server. +
    To use this function, you need to contact [support@agora.io](mailto:support@agora.io) to enable it.
    + ```objective-c // Delete messages by timestamp AgoraChatConversation* conversation = [AgoraChatClient.sharedClient.chatManager getConversationWithConvId:@"conversationId"]; diff --git a/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/react-native.mdx b/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/react-native.mdx index 32f499ccc..76dbe35dd 100644 --- a/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/react-native.mdx +++ b/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/react-native.mdx @@ -6,6 +6,7 @@ Call `fetchConversationsFromServerWithCursor` to retrieve conversations from the The server stores 100 conversations for 7 days by default. To increase the two upper limits, contact support@agora.io. Agora Chat server can store up to 3,000 conversation per end user. ```java +// pageSize: The number of conversations that you expect to get on each page. The value range is [1,50]. // cursor: If `cursor` is an empty string, the SDK retrieves from the latest conversation. ChatClient.getInstance() .chatManager.fetchConversationsFromServerWithCursor(cursor, pageSize) @@ -17,7 +18,7 @@ ChatClient.getInstance() }); ``` -For users that do not support `fetchConversationsFromServerWithPage`, call `fetchAllConversations` to retrieve the conversations from the server. By default, the SDK retrieves the last ten conversations in the past seven days, and each conversation contains one last historical message. To adjust the time limit or the number of conversations retrieved, contact [support@agora.io](mailto:support@agora.io). +If you do not support `fetchConversationsFromServerWithCursor`, call `fetchConversationsFromServerWithPage` to retrieve the conversations from the server. Altogether, the SDK can retrieve the last 100 conversations in the past seven days. To adjust the time limit or the number of conversations retrieved, contact support@agora.io. ### Retrieve historical messages of the specified conversation @@ -25,7 +26,7 @@ After retrieving conversations, you can retrieve historical messages from the se You can set the search direction to retrieve messages in the chronological or reverse chronological order of when the server receives them, the message type, the time period, the message sender, as well as whether to save the retrieved message to the local database. -If you have implemented Chat SDK after June 8, 2023, you can retrieve historical messages even before joining the Chat Group. For earlier implementations, contact [support@agora.io](mailto:support@agora.io) to enable this. +If you have integrated Chat SDK after June 8, 2023, you can retrieve historical messages even before joining the Chat Group. For earlier implementations, contact [support@agora.io](mailto:support@agora.io) to enable this. The Agora Chat server stores the full message history for a certain period of time depending on your subscribed [Chat plan](../../reference/message-overview#limitations-of-message-storage-duration). After an end user logs back into Agora Chat, the servers automatically send offline messages to them, that is, messages transmitted when that end user was offline. Offline messages are a subset of the full message history stored on Agora Chat server. Sending only a subset of messages prevents distributing too many messages to a single device, which can overwhelm it and slow down the end user login. Agora Chat server stores and manages these offline messages for every end user in the following way: @@ -99,6 +100,8 @@ ChatClient.getInstance() Call `removeMessagesFromServerWithTimestamp` or `removeMessagesFromServerWithMsgIds` to delete historical messages one way from the server. You can remove a maximum of 50 messages from the server each time. Once the messages are deleted, you can no longer retrieve them from the server. The deleted messages are automatically removed from your local device. Other chat users can still get the messages from the server. +
    To use this function, you need to contact [support@agora.io](mailto:support@agora.io) to enable it.
    + ```typescript // Delete messages by message ID ChatClient.getInstance() diff --git a/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/unity.mdx b/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/unity.mdx index 0d13ff180..50e97c8ac 100644 --- a/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/unity.mdx +++ b/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/unity.mdx @@ -10,6 +10,7 @@ In the conversation list, each conversation object contains the conversation ID, The server stores 100 conversations for 7 days by default. To increase the two upper limits, contact support@agora.io. Agora Chat server can store up to 3,000 conversation per end user. ```csharp +// limit: The number of conversations that you expect to get on each page. The value range is [1,50]. int limit = 10; string cursor = ""; bool pinOnly = false; // `false`:Get the list of all conversations; `true`: Get the list of only pinned conversations. @@ -31,15 +32,13 @@ SDKClient.Instance.ChatManager.GetConversationsFromServerWithCursor(pinOnly, cur )); ``` -For users that do not support `GetConversationsFromServerWithPage`, call `GetConversationsFromServer` to retrieve all the conversations from the server. By default, the SDK retrieves the last ten conversations in the past seven days, and each conversation contains one last historical message. To adjust the time limit or the number of conversations retrieved, contact [support@agora.io](mailto:support@agora.io). - ### Retrieve historical messages of the specified conversation After retrieving conversations, you can retrieve historical messages from the server. You can set the search direction to retrieve messages in the chronological or reverse chronological order of when the server receives them, the message type, the time period, the message sender, as well as whether to save the retrieved message to the local database. -If you have implemented Chat SDK after June 8, 2023, you can retrieve historical messages even before joining the Chat Group. For earlier implementations, contact [support@agora.io](mailto:support@agora.io) to enable this. +If you have integrated Chat SDK after June 8, 2023, you can retrieve historical messages even before joining the Chat Group. For earlier implementations, contact [support@agora.io](mailto:support@agora.io) to enable this. The Agora Chat server stores the full message history for a certain period of time depending on your subscribed [Chat plan](../../reference/message-overview#limitations-of-message-storage-duration). After an end user logs back into Agora Chat, the servers automatically send offline messages to them, that is, messages transmitted when that end user was offline. Offline messages are a subset of the full message history stored on Agora Chat server. Sending only a subset of messages prevents distributing too many messages to a single device, which can overwhelm it and slow down the end user login. Agora Chat server stores and manages these offline messages for every end user in the following way: @@ -132,6 +131,9 @@ SDKClient.Instance.ChatManager.GetConversationsFromServerWithCursor(pinOnly, cur Call `RemoveMessagesFromServer` to delete historical messages one way from the server. You can remove a maximum of 50 messages from the server each time. Once the messages are deleted, you can no longer retrieve them from the server. The deleted messages are automatically removed from your local device. Other chat users can still get the messages from the server. +
    To use this function, you need to contact [support@agora.io](mailto:support@agora.io) to enable it.
    + + ```csharp SDKClient.Instance.ChatManager.RemoveMessagesFromServer(convId, ctype, time, new CallBack( onSuccess: () => diff --git a/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/web.mdx b/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/web.mdx index 255856c8b..73e079219 100644 --- a/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/web.mdx +++ b/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/web.mdx @@ -14,15 +14,13 @@ connection.getServerConversations({pageSize:50, cursor: ''}).then((res)=>{ }) ``` -If you still use the `getConversationlist` method to retrieve the conversations from the server without pagination, the SDK, by default, retrieves the last ten conversations in the past seven days, and each conversation contains one last historical message. To adjust the time limit or the number of conversations retrieved, contact [support@agora.io](mailto:support@agora.io). - ### Retrieve historical messages of the specified conversation After retrieving conversations, you can retrieve historical messages from the server. You can set the search direction to retrieve messages in the chronological or reverse chronological order of when the server receives them, the message type, the time period, the message sender, as well as whether to save the retrieved message to the local database. -If you have implemented Chat SDK after June 8, 2023, you can retrieve historical messages even before joining the Chat Group. For earlier implementations, contact [support@agora.io](mailto:support@agora.io) to enable this. +If you have integrated Chat SDK after June 8, 2023, you can retrieve historical messages even before joining the Chat Group. For earlier implementations, contact [support@agora.io](mailto:support@agora.io) to enable this. The Agora Chat server stores the full message history for a certain period of time depending on your subscribed [Chat plan](../../reference/message-overview#limitations-of-message-storage-duration). After an end user logs back into Agora Chat, the servers automatically send offline messages to them, that is, messages transmitted when that end user was offline. Offline messages are a subset of the full message history stored on Agora Chat server. Sending only a subset of messages prevents distributing too many messages to a single device, which can overwhelm it and slow down the end user login. Agora Chat server stores and manages these offline messages for every end user in the following way: @@ -78,6 +76,8 @@ connection.getServerPinnedConversations({pageSize:50, cursor: ''}) Call `removeHistoryMessages` to delete historical messages one way from the server. You can remove a maximum of 20 messages from the server each time. Once the messages are deleted, you can no longer retrieve them from the server. Other chat users can still get the messages from the server. +
    To use this function, you need to contact [support@agora.io](mailto:support@agora.io) to enable it.
    + ```javascript // Delete messages by timestamp connection.removeHistoryMessages({targetId: 'userId', chatType: 'singleChat', beforeTimeStamp: Date.now()}) diff --git a/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/windows.mdx b/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/windows.mdx index 57405185f..bfd508d13 100644 --- a/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/windows.mdx +++ b/shared/chat-sdk/client-api/messages/retrieve-messages/project-implementation/windows.mdx @@ -10,6 +10,7 @@ In the conversation list, each conversation object contains the conversation ID, The server stores 100 conversations for 7 days by default. To increase the two upper limits, contact support@agora.io. Agora Chat server can store up to 3,000 conversation per end user. ```csharp +// limit: The number of conversations that you expect to get on each page. The value range is [1,50]. int limit = 10; string cursor = ""; bool pinOnly = false; // `false`:Get all conversations; `true`: Get the list of pinned conversations. @@ -31,15 +32,13 @@ SDKClient.Instance.ChatManager.GetConversationsFromServerWithCursor(pinOnly, cur )); ``` -For users that do not support `GetConversationsFromServerWithPage`, call `GetConversationsFromServer` to retrieve all the conversations from the server. By default, the SDK retrieves the last ten conversations in the past seven days, and each conversation contains one last historical message. To adjust the time limit or the number of conversations retrieved, contact [support@agora.io](mailto:support@agora.io). - ### Retrieve historical messages of the specified conversation After retrieving conversations, you can retrieve historical messages from the server. You can set the search direction to retrieve messages in the chronological or reverse chronological order of when the server receives them, the message type, the time period, the message sender, as well as whether to save the retrieved message to the local database. -If you have implemented Chat SDK after June 8, 2023, you can retrieve historical messages even before joining the Chat Group. For earlier implementations, contact [support@agora.io](mailto:support@agora.io) to enable this. +If you have integrated Chat SDK after June 8, 2023, you can retrieve historical messages even before joining the Chat Group. For earlier implementations, contact [support@agora.io](mailto:support@agora.io) to enable this. The Agora Chat server stores the full message history for a certain period of time depending on your subscribed [Chat plan](../../reference/message-overview#limitations-of-message-storage-duration). After an end user logs back into Agora Chat, the servers automatically send offline messages to them, that is, messages transmitted when that end user was offline. Offline messages are a subset of the full message history stored on Agora Chat server. Sending only a subset of messages prevents distributing too many messages to a single device, which can overwhelm it and slow down the end user login. Agora Chat server stores and manages these offline messages for every end user in the following way: @@ -131,6 +130,8 @@ SDKClient.Instance.ChatManager.GetConversationsFromServerWithCursor(pinOnly, cur Call `RemoveMessagesFromServer` to delete historical messages one way from the server. You can remove a maximum of 50 messages from the server each time. Once the messages are deleted, you can no longer retrieve them from the server. The deleted messages are automatically removed from your local device. Other chat users can still get the messages from the server. +
    To use this function, you need to contact [support@agora.io](mailto:support@agora.io) to enable it.
    + ```csharp SDKClient.Instance.ChatManager.RemoveMessagesFromServer(convId, ctype, time, new CallBack( onSuccess: () => diff --git a/shared/chat-sdk/client-api/messages/send-receive-messages/project-implementation/ios.mdx b/shared/chat-sdk/client-api/messages/send-receive-messages/project-implementation/ios.mdx index 49bf7df12..7307577ea 100644 --- a/shared/chat-sdk/client-api/messages/send-receive-messages/project-implementation/ios.mdx +++ b/shared/chat-sdk/client-api/messages/send-receive-messages/project-implementation/ios.mdx @@ -91,7 +91,7 @@ You can also use `messagesDidRecall` to listen for the message recall state: /** * Occurs when a received message is recalled. */ -- (void)messagesInfoDidRecall:(NSArray * _Nonnull)aRecallMessagesInfo; +- (void)messagesInfoDidRecall:(NSArray * _Nonnull)aRecallMessagesInfo; ``` ### Send and receive an attachment message diff --git a/shared/chat-sdk/client-api/reaction/project-implementation/android.mdx b/shared/chat-sdk/client-api/reaction/project-implementation/android.mdx index de23087ea..53b848eeb 100644 --- a/shared/chat-sdk/client-api/reaction/project-implementation/android.mdx +++ b/shared/chat-sdk/client-api/reaction/project-implementation/android.mdx @@ -2,7 +2,9 @@ ### Add a reaction -Call `asyncAddReaction` to add a reaction to the specified message. You can use `onReactionChanged` to listen for the state of adding the reaction. +Call `asyncAddReaction` to add a reaction to the specified message. In a one-to-one conversation, the peer user receives the `onReactionChanged` event. In a group conversation, other group members than the operator receive this event. This event contains the conversation ID, message ID, reaction list of the message, and reaction operation list (ID of the new reaction, user ID of the user that adds the reaction, and the addition operation). + +For a reaction, a user can add only once, or the error 1301 is reported for repeated addition. ```java // Add a reaction @@ -33,7 +35,7 @@ public class MyClass implements MessageListener { ### Remove a reaction -Call `asyncRemoveReaction` to remove the specified reaction. You can also listen for the reaction change in `onReactionChanged`. +Call `asyncRemoveReaction` to remove the specified reaction. In a one-to-one conversation, the peer user receives the `onReactionChanged` event. In a group conversation, other group members than the operator receive this event. This event contains the conversation ID, message ID, reaction list of the message, and reaction operation list (ID of the removed reaction, user ID of the user that removes the reaction, and the removal operation). ```java // Remove the reaction. @@ -64,7 +66,7 @@ public class MyClass implements MessageListener { ### Retrieve a list of reactions -Call `asyncGetReactionList` to retrieve a list of reactions from the server. This method also returns the basic information of the reactions, including the content of the reaction, the number of users that added or removed the reaction, and a list of the first three user IDs that added or removed the reaction. +Call `asyncGetReactionList` to retrieve a list of reactions of messages from the server. This method also returns the basic information of the reactions, including the content of the reaction, the number of users that added or removed the reaction, and a list of the first three user IDs that added or removed the reaction. ```java ChatClient.getInstance().chatManager().asyncGetReactionList(msgIdList, ChatMessage.ChatType.Chat, groupId, new ValueCallBack>>() { diff --git a/shared/chat-sdk/client-api/reaction/project-implementation/flutter.mdx b/shared/chat-sdk/client-api/reaction/project-implementation/flutter.mdx index f42a33a5e..56d2cafcc 100644 --- a/shared/chat-sdk/client-api/reaction/project-implementation/flutter.mdx +++ b/shared/chat-sdk/client-api/reaction/project-implementation/flutter.mdx @@ -2,7 +2,9 @@ ### Add a reaction -Call `addReaction` to add a reaction to the specified message. You can use `onMessageReactionDidChange` to listen for the state of adding the reaction. +Call `addReaction` to add a reaction to the specified message. In a one-to-one conversation, the peer user receives the `onMessageReactionDidChange` event. In a group conversation, other group members than the operator receive this event. This event contains the conversation ID, message ID, reaction list of the message, and reaction operation list (reaction ID, user ID of the user that adds the reaction, and the addition operation). + +For a reaction, a user can add only once, or the error 1301 is reported for repeated addition. ```dart // reaction: Reaction ID @@ -20,7 +22,7 @@ ChatClient.getInstance() ### Remove a reaction -Call `removeReaction` to remove the specified reaction. You can also listen for the reaction change in `onMessageReactionDidChange`. +Call `removeReaction` to remove the specified reaction. In a one-to-one conversation, the peer user receives the `onMessageReactionDidChange` event. In a group conversation, other group members than the operator receive this event. This event contains the conversation ID, message ID, reaction list of the message, and reaction operation list (ID of the removed reaction, user ID of the user that removes the reaction, and the removal operation). ```dart try { @@ -34,7 +36,7 @@ try { ### Retrieve a list of reactions -Call `getReactionList` to retrieve a list of reactions from the server. This method also returns the basic information of the reactions, including the content of the reaction, the number of users that added or removed the reaction, and a list of the first three user IDs that added or removed the reaction. +Call `getReactionList` to retrieve a list of reactions of messages from the server. This method also returns the basic information of the reactions, including the content of the reaction, the number of users that added or removed the reaction, and a list of the first three user IDs that added or removed the reaction. ```dart try { diff --git a/shared/chat-sdk/client-api/reaction/project-implementation/ios.mdx b/shared/chat-sdk/client-api/reaction/project-implementation/ios.mdx index 3da806e17..b30e5eeec 100644 --- a/shared/chat-sdk/client-api/reaction/project-implementation/ios.mdx +++ b/shared/chat-sdk/client-api/reaction/project-implementation/ios.mdx @@ -2,7 +2,9 @@ ### Add a reaction -Call `addReaction` to add a reaction to the specified message. You can use `messageReactionDidChange` to listen for the state of adding the reaction. +Call `addReaction` to add a reaction to the specified message. In a one-to-one conversation, the peer user receives the `messageReactionDidChange` event. In a group conversation, other group members than the operator receive this event. This event contains the conversation ID, message ID, reaction list of the message, and reaction operation list (reaction ID, user ID of the user that adds the reaction, and the addition operation). + +For a reaction, a user can add only once, or the error 1301 is reported for repeated addition. ```objc // Add a reaction @@ -18,7 +20,7 @@ Call `addReaction` to add a reaction to the specified message. You can use `mess ### Remove a reaction -Call `removeReaction` to remove the specified reaction. You can also listen for the reaction change in `messageReactionDidChange`. +Call `removeReaction` to remove the specified reaction. In a one-to-one conversation, the peer user receives the `messageReactionDidChange` event. In a group conversation, other group members than the operator receive this event. This event contains the conversation ID, message ID, reaction list of the message, and reaction operation list (ID of the removed reaction, user ID of the user that removes the reaction, and the removal operation). ```objc // Remove the reaction. @@ -34,7 +36,7 @@ Call `removeReaction` to remove the specified reaction. You can also listen for ### Retrieve a list of reactions -Call `getReactionList` to retrieve a list of reactions from the server. This method also returns the basic information of the reactions, including the content of the reaction, the number of users that added or removed the reaction, and a list of the first three user IDs that added or removed the reaction. +Call `getReactionList` to retrieve a list of reactions of messages from the server. This method also returns the basic information of the reactions, including the content of the reaction, the number of users that added or removed the reaction, and a list of the first three user IDs that added or removed the reaction. ```objc [AgoraChatClient.sharedClient.chatManager getReactionList:@["messageId"] groupId:@"groupId" chatType:AgoraChatTypeChat completion:^(NSDictionary * _Nonnull, AgoraChatError * _Nullable) { diff --git a/shared/chat-sdk/client-api/reaction/project-implementation/react-native.mdx b/shared/chat-sdk/client-api/reaction/project-implementation/react-native.mdx index faff5a209..2a1f6652e 100644 --- a/shared/chat-sdk/client-api/reaction/project-implementation/react-native.mdx +++ b/shared/chat-sdk/client-api/reaction/project-implementation/react-native.mdx @@ -2,7 +2,9 @@ ### Add a reaction -Call `addReaction` to add a reaction to the specified message. You can use `onMessageReactionDidChange` to listen for the state of adding the reaction. +Call `addReaction` to add a reaction to the specified message. In a one-to-one conversation, the peer user receives the `onMessageReactionDidChange` event. In a group conversation, other group members than the operator receive this event. This event contains the conversation ID, message ID, reaction list of the message, and reaction operation list (reaction ID, user ID of the user that adds the reaction, and the addition operation). + +For a reaction, a user can add only once, or the error 1301 is reported for repeated addition. ```typescript // reaction: Reaction ID @@ -20,8 +22,7 @@ ChatClient.getInstance() ### Remove a reaction -Call `removeReaction` to remove the specified reaction. You can also listen for the reaction change in `onMessageReactionDidChange`. - +Call `removeReaction` to remove the specified reaction. You can also listen for the reaction change in `onMessageReactionDidChange`. In a one-to-one conversation, the peer user receives the `onMessageReactionDidChange` event. In a group conversation, other group members than the operator receive this event. This event contains the conversation ID, message ID, reaction list of the message, and reaction operation list (ID of the removed reaction, user ID of the user that removes the reaction, and the removal operation). ```typescript // reaction: Reaction ID // msgId: The message ID @@ -37,8 +38,7 @@ ChatClient.getInstance() ### Retrieve a list of reactions -Call `getReactionList` to retrieve a list of reactions from the server. This method also returns the basic information of the reactions, including the content of the reaction, the number of users that added or removed the reaction, and a list of the first three user IDs that added or removed the reaction. - +Call `getReactionList` to retrieve a list of reactions of messages from the server. This method also returns the basic information of the reactions, including the content of the reaction, the number of users that added or removed the reaction, and a list of the first three user IDs that added or removed the reaction. ```typescript // msgId: The message ID ChatClient.getInstance() diff --git a/shared/chat-sdk/client-api/reaction/project-implementation/unity.mdx b/shared/chat-sdk/client-api/reaction/project-implementation/unity.mdx index 72a10f606..e4cbc05e6 100644 --- a/shared/chat-sdk/client-api/reaction/project-implementation/unity.mdx +++ b/shared/chat-sdk/client-api/reaction/project-implementation/unity.mdx @@ -2,7 +2,9 @@ ### Add a reaction -Call `AddReaction` to add a reaction to the specified message. You can use `MessageReactionDidChange` to listen for the state of adding the reaction. +Call `AddReaction` to add a reaction to the specified message. In a one-to-one conversation, the peer user receives the `MessageReactionDidChange` event. In a group conversation, other group members than the operator receive this event. This event contains the conversation ID, message ID, reaction list of the message, and reaction operation list (reaction ID, user ID of the user that adds the reaction, and the addition operation). + +For a reaction, a user can add only once, or the error 1301 is reported for repeated addition. ```csharp // Adds a reaction to the specified message @@ -37,8 +39,7 @@ SDKClient.Instance.ChatManager.AddReactionManagerDelegate(reactionManagerDelegat ### Remove a reaction -Call `RemoveReaction` to remove the specified reaction. You can also listen for the reaction change in `MessageReactionDidChange`. - +Call `RemoveReaction` to remove the specified reaction. In a one-to-one conversation, the peer user receives the `MessageReactionDidChange` event. In a group conversation, other group members than the operator receive this event. This event contains the conversation ID, message ID, reaction list of the message, and reaction operation list (ID of the removed reaction, user ID of the user that removes the reaction, and the removal operation). ```csharp // Removes the reaction. SDKClient.Instance.ChatManager.RemoveReaction(msg_id, reaction, new CallBack( @@ -72,8 +73,7 @@ SDKClient.Instance.ChatManager.AddReactionManagerDelegate(reactionManagerDelegat ### Retrieve a list of reactions -Call `GetReactionList` to retrieve a list of reactions from the server. This method also returns the basic information of the reactions, including the content of the reaction, the number of users that added or removed the reaction, and a list of the first three user IDs that added or removed the reaction. - +Call `GetReactionList` to retrieve a list of reactions of messages from the server. This method also returns the basic information of the reactions, including the content of the reaction, the number of users that added or removed the reaction, and a list of the first three user IDs that added or removed the reaction. ```csharp SDKClient.Instance.ChatManager.GetReactionList(messageIdList, chatType, groupId, new ValueCallBack>>( onSuccess: (dict) => diff --git a/shared/chat-sdk/client-api/reaction/project-implementation/web.mdx b/shared/chat-sdk/client-api/reaction/project-implementation/web.mdx index a1b472a93..dc826ced8 100644 --- a/shared/chat-sdk/client-api/reaction/project-implementation/web.mdx +++ b/shared/chat-sdk/client-api/reaction/project-implementation/web.mdx @@ -2,7 +2,9 @@ ### Add a reaction -Call `addReaction` to add a reaction to the specified message. You can use `onReactionChange` to listen for the state of adding the reaction. +Call `addReaction` to add a reaction to the specified message. In a one-to-one conversation, the peer user receives the `onReactionChange` event. In a group conversation, other group members than the operator receive this event. This event contains the conversation ID, message ID, reaction list of the message, and reaction operation list (reaction ID, user ID of the user that adds the reaction, and the addition operation). + +For a reaction, a user can add only once, or the error 1101 is reported for repeated addition. ```javascript // Add a reaction @@ -17,7 +19,7 @@ conn.addEventHandler("REACTION", { ### Remove a reaction -Call `deleteReaction` to remove the specified reaction. You can also listen for the reaction change in `onReactionChange`. +Call `deleteReaction` to remove the specified reaction. In a one-to-one conversation, the peer user receives the `onReactionChanged` event. In a group conversation, other group members than the operator receive this event. This event contains the conversation ID, message ID, reaction list of the message, and reaction operation list (ID of the removed reaction, user ID of the user that removes the reaction, and the removal operation). ```javascript // Remove the reaction. @@ -32,7 +34,7 @@ conn.addEventHandler("REACTION", { ### Retrieve a list of reactions -Call `getReactionList` to retrieve a list of reactions from the server. This method also returns the basic information of the reactions, including the content of the reaction, the number of users that added or removed the reaction, and a list of the first three user IDs that added or removed the reaction. +Call `getReactionList` to retrieve a list of reactions of messages from the server. This method also returns the basic information of the reactions, including the content of the reaction, the number of users that added or removed the reaction, and a list of the first three user IDs that added or removed the reaction. ```javascript conn diff --git a/shared/chat-sdk/client-api/reaction/project-implementation/windows.mdx b/shared/chat-sdk/client-api/reaction/project-implementation/windows.mdx index 9d7142f53..f5bc2e785 100644 --- a/shared/chat-sdk/client-api/reaction/project-implementation/windows.mdx +++ b/shared/chat-sdk/client-api/reaction/project-implementation/windows.mdx @@ -2,7 +2,9 @@ ### Add a reaction -Call `AddReaction` to add a reaction to the specified message. You can use `MessageReactionDidChange` to listen for the state of adding the reaction. +Call `AddReaction` to add a reaction to the specified message. In a one-to-one conversation, the peer user receives the `MessageReactionDidChange` event. In a group conversation, other group members than the operator receive this event. This event contains the conversation ID, message ID, reaction list of the message, and reaction operation list (reaction ID, user ID of the user that adds the reaction, and the addition operation). + +For a reaction, a user can add only once, or the error 1301 is reported for repeated addition. ```csharp // Adds a reaction to the specified message @@ -37,7 +39,7 @@ SDKClient.Instance.ChatManager.AddReactionManagerDelegate(reactionManagerDelegat ### Remove a reaction -Call `RemoveReaction` to remove the specified reaction. You can also listen for the reaction change in `MessageReactionDidChange`. +Call `RemoveReaction` to remove the specified reaction. In a one-to-one conversation, the peer user receives the `MessageReactionDidChange` event. In a group conversation, other group members than the operator receive this event. This event contains the conversation ID, message ID, reaction list of the message, and reaction operation list (ID of the removed reaction, user ID of the user that removes the reaction, and the removal operation). ```csharp // Removes the reaction. @@ -72,8 +74,7 @@ SDKClient.Instance.ChatManager.AddReactionManagerDelegate(reactionManagerDelegat ### Retrieve a list of reactions -Call `GetReactionList` to retrieve a list of reactions from the server. This method also returns the basic information of the reactions, including the content of the reaction, the number of users that added or removed the reaction, and a list of the first three user IDs that added or removed the reaction. - +Call `GetReactionList` to retrieve a list of reactions of messages from the server. This method also returns the basic information of the reactions, including the content of the reaction, the number of users that added or removed the reaction, and a list of the first three user IDs that added or removed the reaction. ```csharp SDKClient.Instance.ChatManager.GetReactionList(messageIdList, chatType, groupId, new ValueCallBack>>( onSuccess: (dict) => diff --git a/shared/chat-sdk/client-api/threading/_thread-management.mdx b/shared/chat-sdk/client-api/threading/_thread-management.mdx index 6536da691..cd1fe1718 100644 --- a/shared/chat-sdk/client-api/threading/_thread-management.mdx +++ b/shared/chat-sdk/client-api/threading/_thread-management.mdx @@ -56,7 +56,7 @@ Before proceeding, ensure that you meet the following requirements: - You have initialized the Chat SDK. For details, see . - You understand the call frequency limit of the Chat APIs supported by different pricing plans as described in [Limitations](/agora-chat/reference/limitations). -
    The thread feature is supported by all types of [Pricing Plans](/agora-chat/reference/pricing-plan-details) and is enabled by default once you have enabled Chat in Agora Console.
    +
    The thread feature is supported by all types of [Pricing Plans](/agora-chat/reference/pricing-plan-details) and is enabled by default once you have enabled Chat in .
    ## Implementation diff --git a/shared/chat-sdk/client-api/threading/_thread-messages.mdx b/shared/chat-sdk/client-api/threading/_thread-messages.mdx index a0a953f04..bd8bd4936 100644 --- a/shared/chat-sdk/client-api/threading/_thread-messages.mdx +++ b/shared/chat-sdk/client-api/threading/_thread-messages.mdx @@ -33,7 +33,7 @@ Before proceeding, ensure that you meet the following requirements: - You have initialized the Chat SDK. For details, see . - You understand the call frequency limit of the Chat APIs supported by different pricing plans as described in [Limitations](/agora-chat/reference/limitations). -
    The thread feature is supported by all types of [Pricing Plans](/agora-chat/reference/pricing-plan-details) and is enabled by default once you have enabled Chat in Agora Console.
    +
    The thread feature is supported by all types of [Pricing Plans](/agora-chat/reference/pricing-plan-details) and is enabled by default once you have enabled Chat in .
    ## Implementation diff --git a/shared/chat-sdk/client-api/threading/thread-messages/project-implementation/flutter.mdx b/shared/chat-sdk/client-api/threading/thread-messages/project-implementation/flutter.mdx index 8ac75b32e..8b217f746 100644 --- a/shared/chat-sdk/client-api/threading/thread-messages/project-implementation/flutter.mdx +++ b/shared/chat-sdk/client-api/threading/thread-messages/project-implementation/flutter.mdx @@ -5,14 +5,13 @@ Sending a thread message is similar to sending a message in a chat group. The difference lies in the `isChatThreadMessage` field, as shown in the following code sample: ```dart -// Sets `chatThreadId` to thread ID. +// Sets `targetId` to thread ID. // Sets `content` to the message content. ChatMessage msg = ChatMessage.createTxtSendMessage( - chatThreadId: threadId, + targetId: threadId, content: content, + chatType: ChatType.GroupChat, ); -// Sets `ChatType` to GroupChat as a thread belongs to a chat group. -msg.chatType = ChatType.GroupChat; // Sets `isChatThreadMessage` to `true` to mark this message as a thread message. msg.isChatThreadMessage = true; // Sends the message. @@ -77,10 +76,11 @@ Once a message is recalled in a thread, all chat group members receive the `Chat ChatClient.getInstance.chatThreadManager.removeEventHandler("UNIQUE_HANDLER_ID"); ``` + + ### Retrieve thread messages You can retrieve thread messages locally or from the server, depending on your production environment. - You can check `ChatConversation#isChatThread()` to determine whether the current conversation is a thread conversation. #### Retrieve messages of a thread from the server @@ -108,9 +108,9 @@ try { } ``` -#### Retrieve messages of a thread locally +### Retrieve messages of a thread locally -By calling [`loadAllConversations`](/agora-chat/client-api/messages/manage-messages#retrieve-conversations), you can only retrieve local one-to-one chat conversations and group conversations. To retrieve messages of a thread locally, refer to the following code sample: +By calling [`loadAllConversations`](../messages/manage-messages#retrieve-conversations), you can only retrieve local one-to-one chat conversations and group conversations. To retrieve messages of a thread locally, refer to the following code sample: ```dart try { @@ -119,7 +119,7 @@ try { // The conversation type is set to `GroupChat` as a thread belongs a group conversation. ChatConversationType convType = ChatConversationType.GroupChat; ChatConversation? conversation = await ChatClient.getInstance.chatManager - .getConversation(threadId, type: convType); + .getThreadConversation(threadId); // The starting message for retrieving. String startMsgId = "startMsgId"; // The number of messages that you expect to retrieve on each page. diff --git a/shared/chat-sdk/develop/_authentication.mdx b/shared/chat-sdk/develop/_authentication.mdx index aaac86990..c75b06999 100644 --- a/shared/chat-sdk/develop/_authentication.mdx +++ b/shared/chat-sdk/develop/_authentication.mdx @@ -125,7 +125,19 @@ The following figure shows the API call sequence of generating an Agora Chat tok 3. In `/src/main/resource`, create an `application.properties` file to store the information for generating tokens and update it with your project information and token validity period. For example, set `expire.second` as `6000`, which means the token is valid for 6000 seconds. - ``` shellscript + 1. Download the [chat](https://github.com/AgoraIO/Tools/tree/master/DynamicKey/AgoraDynamicKey/java/src/main/java/io/agora/chat) and [media](https://github.com/AgoraIO/Tools/tree/master/DynamicKey/AgoraDynamicKey/java/src/main/java/io/agora/media) packages. + 2. In your token server project, create a `com.agora.chat.token.io.agora` package under `/src/main/java`. + 3. Copy the `chat` and `media` packages and paste them under `com.agora.chat.token.io.agora`. Now the project structure is as following screenshot shows: + ![](https://web-cdn.agora.io/docs-files/1638864182234) + 4. Fix the import errors in the `chat/ChatTokenBuilder2` and `media/AccessToken` files. + - In `ChatTokenBuilder2`, change `package io.agora.chat;` to `package com.agora.chat.token.io.agora.chat;` and change `import io.agora.media.AccessToken2;` to `import com.agora.chat.token.io.agora.media.AccessToken2;`. + - In all files of the `com.agora.chat.token.io.agora.media` package, change `package io.agora.media;` to `package com.agora.chat.token.io.agora.media;`. + - In `AccessToken`, change `import static io.agora.media.Utils.crc32;` to `import static com.agora.chat.token.io.agora.media.Utils.crc32;`. + + +4. In `/src/main/resource`, create an `application.properties` file to store the information for generating tokens and update it with your project information and token validity period. For example, set `expire.second` as `6000`, which means the token is valid for 6000 seconds. + + ```shellscript ## Server port. server.port=8090 ## Fill the App ID of your Agora project. diff --git a/shared/chat-sdk/develop/_content-moderation.mdx b/shared/chat-sdk/develop/_content-moderation.mdx index 08f6abc9c..0cb9d9c11 100644 --- a/shared/chat-sdk/develop/_content-moderation.mdx +++ b/shared/chat-sdk/develop/_content-moderation.mdx @@ -10,8 +10,8 @@ Delivering a safe and appropriate chat environment to your users is essential. C ## Prerequisites - You have created a valid [Agora developer account](../reference/manage-agora-account#create-an-agora-account). -- Moderation is not enabled by default. To use this feature, you need to subscribe to the **Pro** or **Enterprise** [pricing plan](../reference/pricing-plan-details) and enable it in [Agora Console](https://console.agora.io/). -
    Add-on fees are incurred if you use this feature. See Pricing for details.
    +- Moderation is not enabled by default. To use this feature, you need to subscribe to the **Pro** or **Enterprise** [pricing plan](../reference/pricing-plan-details) and enable it in . +
    Add-on fees are incurred if you use this feature. See Pricing for details.
    ## Enable the moderation feature @@ -121,7 +121,7 @@ Follow these steps to specify a profanity filter configuration: ![keyword_en](https://web-cdn.agora.io/docs-files/1656313419195) -2. On the **Rule Config** page, you can add or delete words and determine which filtering method to apply to messages that contain the specified keywords. You can replace the word with \*\*\* or simply not send the word. +2. On the **Rule Config** page, you can add or delete words and determine which filtering method to apply to messages that contain the specified keywords. You can replace the word with \*\*\* or simply not send the word. You can add up to 10,000 words to the list. Contact [support@agora.io](mailto:support@agora.io) if you need to extend this limit to 100,000 words. ### Domain filter diff --git a/shared/chat-sdk/develop/_setup-webhooks.mdx b/shared/chat-sdk/develop/_setup-webhooks.mdx index 0002b2728..348eb9cb9 100644 --- a/shared/chat-sdk/develop/_setup-webhooks.mdx +++ b/shared/chat-sdk/develop/_setup-webhooks.mdx @@ -53,7 +53,7 @@ To use the HTTP callbacks, you must meet the following requirements: - You have an Agora project with Chat [enabled](../get-started/enable). - You understand the API call frequency limit as described in [Limitations](../reference/limitations). -- The HTTP callback feature is not enabled by default. To use this feature, you need to subscribe to the **Pro** or **Enterprise** [pricing plan](../reference/pricing-plan-details) and enable it in [Agora Console](https://console.agora.io/). +- The HTTP callback feature is not enabled by default. To use this feature, you need to subscribe to the **Pro** or **Enterprise** [pricing plan](../reference/pricing-plan-details) and enable it in .
    You must contact support@agora.io to disable this feature as this operation will delete all the relevant configurations.
    ## Configure callback rules diff --git a/shared/chat-sdk/develop/offline-push/project-implementation/android.mdx b/shared/chat-sdk/develop/offline-push/project-implementation/android.mdx index 098ba0704..887ebbda8 100644 --- a/shared/chat-sdk/develop/offline-push/project-implementation/android.mdx +++ b/shared/chat-sdk/develop/offline-push/project-implementation/android.mdx @@ -28,7 +28,7 @@ This section guides you through how to integrate FCM with Chat. ### 2. Upload FCM certificate to Agora Console -1. Log in to [Agora Console](https://console.agora.io/), and click **Project Management** in the left navigation bar. +1. Log in to , and click **Project Management** in the left navigation bar. 2. On the **Project Management** page, locate the project that has Chat enabled and click **Config**. diff --git a/shared/chat-sdk/develop/offline-push/project-implementation/flutter.mdx b/shared/chat-sdk/develop/offline-push/project-implementation/flutter.mdx index 46010c775..e5ff10a58 100644 --- a/shared/chat-sdk/develop/offline-push/project-implementation/flutter.mdx +++ b/shared/chat-sdk/develop/offline-push/project-implementation/flutter.mdx @@ -25,7 +25,7 @@ This section guides you through how to integrate FCM with Chat. ### 2. Upload FCM certificate to Agora Console -1. Log in to [Agora Console](https://console.agora.io/), and click **Project Management** in the left navigation bar. +1. Log in to , and click **Project Management** in the left navigation bar. 2. On the **Project Management** page, locate the project that has Chat enabled and click **Config**. diff --git a/shared/chat-sdk/develop/offline-push/project-implementation/ios.mdx b/shared/chat-sdk/develop/offline-push/project-implementation/ios.mdx index 16076d00f..81f4b2c83 100644 --- a/shared/chat-sdk/develop/offline-push/project-implementation/ios.mdx +++ b/shared/chat-sdk/develop/offline-push/project-implementation/ios.mdx @@ -49,7 +49,7 @@ Follow these steps to enable the APNs service: Follow the steps to upload the certificates to Agora Console: -1. Log in to [Agora Console](https://console.agora.io/), and click **Project Management** in the left navigation bar. +1. Log in to , and click **Project Management** in the left navigation bar. 2. On the **Project Management** page, locate the project that has Chat enable and click **Config**. diff --git a/shared/chat-sdk/develop/offline-push/project-implementation/react-native.mdx b/shared/chat-sdk/develop/offline-push/project-implementation/react-native.mdx index b83a53d8a..44e554c62 100644 --- a/shared/chat-sdk/develop/offline-push/project-implementation/react-native.mdx +++ b/shared/chat-sdk/develop/offline-push/project-implementation/react-native.mdx @@ -24,7 +24,7 @@ After the project is created, add an `iOS` application or an `Android` applicati ### 2. Upload FCM certificate to Agora Console -1. Log in to [Agora Console](https://console.agora.io/), and click **Project Management** in the left navigation bar. +1. Log in to , and click **Project Management** in the left navigation bar. 2. On the **Project Management** page, locate the project that has Chat enabled and click **Config**. diff --git a/shared/chat-sdk/develop/offline-push/project-implementation/web.mdx b/shared/chat-sdk/develop/offline-push/project-implementation/web.mdx index e65490d84..c014fdac6 100644 --- a/shared/chat-sdk/develop/offline-push/project-implementation/web.mdx +++ b/shared/chat-sdk/develop/offline-push/project-implementation/web.mdx @@ -6,7 +6,7 @@ To optimize user experience when dealing with an influx of push notifications, A **Push notification mode** - +
    @@ -20,7 +20,7 @@ To optimize user experience when dealing with an influx of push notifications, A - + @@ -47,7 +47,7 @@ Alternatively, assume that a DND time period is specified for a conversation, wh You can call `setSilentModeForAll` to set the push notifications at the app level and set the push notification mode and DND mode by specifying the `paramType` field, as shown in the following code sample: -```` javascript +````javascript options // The push notification options. options: { @@ -83,7 +83,7 @@ WebIM.conn.setSilentModeForAll(params) You can call `getSilentModeForAll` to retrieve the push notification settings at the app level, as shown in the following code sample: -```` javascript +````javascript WebIM.conn.getSilentModeForAll() ```` @@ -91,7 +91,7 @@ WebIM.conn.getSilentModeForAll() You can call `setSilentModeForConversation` to set the push notifications for the conversation specified by the `conversationId` and `type` fields, as shown in the following code sample: -``` javascript +```javascript const params = { conversationId: 'test', // The conversation ID. For one-to-one chats, sets to the ID of the peer user. For group chats, sets to the ID of the chat group or chat room. @@ -142,7 +142,7 @@ WebIM.conn.setSilentModeForConversation(params) You can call `getSilentModeForConversation` to retrieve the push notification settings of the specified conversation, as shown in the following code sample: -```` javascript +````javascript const params = { conversationId: 'test', // The conversation ID. For one-to-one chats, sets to the ID of the peer user. For group chats, sets to the ID of the chat group or chat room. type: 'singleChat', // The chat type. Sets the chat type to `singleChat`, `groupChat`, or `chatRoom`. @@ -179,7 +179,7 @@ You can call `clearRemindTypeForConversation` to clear the push notification mod The following code sample shows how to clear the push notification mode of a conversation: -``` javascript +```javascript const params = { conversationId: '12345', // The conversation ID. For one-to-one chats, sets to the ID of the peer user. For group chats, sets to the ID of the chat group or chat room. type: 'groupChat', // The chat type. Sets the chat type to `singleChat`, `groupChat`, or `chatRoom`. diff --git a/shared/chat-sdk/develop/offline-push/project-setup/android.mdx b/shared/chat-sdk/develop/offline-push/project-setup/android.mdx index e40bd4823..fee9361e4 100644 --- a/shared/chat-sdk/develop/offline-push/project-setup/android.mdx +++ b/shared/chat-sdk/develop/offline-push/project-setup/android.mdx @@ -6,7 +6,7 @@ To optimize user experience when dealing with an influx of push notifications, A **Push notification mode** -
    Push Notification Mode
    ATOnly receives push notifications for mentioned messages.Only receives push notifications for mentioned messages. This mode is recommended for group chats. To mention one or more members in a group, you need to pass `em_at_list":["user1", "user2" ...]` for the `ext` field; to mention all members in a group, pass "em_at_list":"all" for the `ext` field.
    NONE
    +
    @@ -20,7 +20,7 @@ To optimize user experience when dealing with an influx of push notifications, A - + @@ -37,7 +37,7 @@ For example, assume that the push notification mode of the app is set to `MENTIO You can specify both the DND duration and DND interval at the app level. During the specified DND time periods, you do not receive any push notifications. -
    Push Notification Mode
    MENTION_ONLYOnly receives push notifications for mentioned messages.Only receives push notifications for mentioned messages. This mode is recommended for group chats. To mention one or more members in a group, you need to pass `em_at_list":["user1", "user2" ...]` for the `ext` field; to mention all members in a group, pass "em_at_list":"all" for the `ext` field.
    NONE
    +
    @@ -238,7 +238,7 @@ Chat allows users to use ready-made templates for push notifications. You can create and provide push templates for users by referring to the following steps: -1. Log in to [Agora Console](https://console.agora.io/), and click **Project Management** in the left navigation bar. +1. Log in to , and click **Project Management** in the left navigation bar. 2. On the **Project Management** page, locate the project that has Chat enable and click **Config**. diff --git a/shared/chat-sdk/develop/offline-push/project-setup/flutter.mdx b/shared/chat-sdk/develop/offline-push/project-setup/flutter.mdx index c3d5b9a34..f93b4cbc7 100644 --- a/shared/chat-sdk/develop/offline-push/project-setup/flutter.mdx +++ b/shared/chat-sdk/develop/offline-push/project-setup/flutter.mdx @@ -6,7 +6,7 @@ To optimize user experience when dealing with an influx of push notifications, A **Push notification mode** -
    Do-not-disturb Parameter
    +
    @@ -20,7 +20,7 @@ To optimize user experience when dealing with an influx of push notifications, A - + @@ -37,7 +37,7 @@ For example, assume that the push notification mode of the app is set to `MENTIO You can specify both the DND duration and DND interval at the app level. During the specified DND time periods, you do not receive any push notifications. -
    Push Notification Mode
    MENTION_ONLYOnly receives push notifications for mentioned messages.Only receives push notifications for mentioned messages. This mode is recommended for group chats. To mention one or more members in a group, you need to pass `em_at_list":["user1", "user2" ...]` for the `ext` field; to mention all members in a group, pass "em_at_list":"all" for the `ext` field.
    NONE
    +
    @@ -285,7 +285,7 @@ Chat allows users to use ready-made templates for push notifications. You can create and provide push templates for users by referring to the following steps: -1. Log in to [Agora Console](https://console.agora.io/), and click **Project Management** in the left navigation bar. +1. Log in to , and click **Project Management** in the left navigation bar. 2. On the **Project Management** page, locate the project that has Chat enable and click **Config**. diff --git a/shared/chat-sdk/develop/offline-push/project-setup/ios.mdx b/shared/chat-sdk/develop/offline-push/project-setup/ios.mdx index 920f34407..b0b93567c 100644 --- a/shared/chat-sdk/develop/offline-push/project-setup/ios.mdx +++ b/shared/chat-sdk/develop/offline-push/project-setup/ios.mdx @@ -6,7 +6,7 @@ To optimize user experience when dealing with an influx of push notifications, A **Push notification mode** -
    Do-not-disturb Parameter
    +
    @@ -20,7 +20,7 @@ To optimize user experience when dealing with an influx of push notifications, A - + @@ -37,7 +37,7 @@ For example, assume that the push notification mode of the app is set to `Mentio You can specify both the DND duration and DND interval at the app level. During the specified DND time periods, you do not receive any push notifications. -
    Push Notification Mode
    MentionOnlyOnly receives push notifications for mentioned messages.Only receives push notifications for mentioned messages. This mode is recommended for group chats. To mention one or more members in a group, you need to pass `em_at_list":["user1", "user2" ...]` for the `ext` field; to mention all members in a group, pass "em_at_list":"all" for the `ext` field.
    None
    +
    @@ -257,9 +257,9 @@ Chat allows users to use ready-made templates for push notifications. You can create and provide push templates for users by referring to the following steps: -1. Log in to [Agora Console](https://console.agora.io/), and click **Project Management** in the left navigation bar. +1. Log in to , and click **Projects** in the left navigation bar. -2. On the **Project Management** page, locate the project that has Chat enable and click **Config**. +2. On the **Projects** page, locate the project that has Chat enable and click **Config**. 3. On the project edit page, click **Config** next to **Chat**. diff --git a/shared/chat-sdk/develop/offline-push/project-setup/react-native.mdx b/shared/chat-sdk/develop/offline-push/project-setup/react-native.mdx index 84fcabfb2..a8561a357 100644 --- a/shared/chat-sdk/develop/offline-push/project-setup/react-native.mdx +++ b/shared/chat-sdk/develop/offline-push/project-setup/react-native.mdx @@ -6,7 +6,7 @@ To optimize user experience when dealing with an influx of push notifications, A **Push notification mode** -
    Do-not-disturb Parameter
    +
    @@ -20,7 +20,7 @@ To optimize user experience when dealing with an influx of push notifications, A - + @@ -28,7 +28,6 @@ To optimize user experience when dealing with an influx of push notifications, A
    Push Notification Mode
    MENTION_ONLYOnly receives push notifications for mentioned messages.Only receives push notifications for mentioned messages. This mode is recommended for group chats. To mention one or more members in a group, you need to pass `em_at_list":["user1", "user2" ...]` for the `ext` field; to mention all members in a group, pass "em_at_list":"all" for the `ext` field.
    NONE
    - The setting of the push notification mode at the conversation level takes precedence over that at the app level, and those conversations that do not have specific settings for the push notification mode inherit the app setting by default. For example, assume that the push notification mode of the app is set to `MENTION_ONLY`, while that of the specified conversation is set to `ALL`. You receive all the push notifications from this conversation, while you only receive the push notifications for mentioned messages from all the other conversations. @@ -37,7 +36,7 @@ For example, assume that the push notification mode of the app is set to `MENTIO You can specify both the DND duration and DND interval at the app level. During the specified DND time periods, you do not receive any push notifications. - +
    @@ -249,9 +248,9 @@ Chat allows users to use ready-made templates for push notifications. You can create and provide push templates for users by referring to the following steps: -1. Log in to [Agora Console](https://console.agora.io/), and click **Project Management** in the left navigation bar. +1. Log in to , and click **Projects** in the left navigation bar. -2. On the **Project Management** page, locate the project that has Chat enable and click **Config**. +2. On the **Projects** page, locate the project that has Chat enable and click **Config**. 3. On the project edit page, click **Config** next to **Chat**. diff --git a/shared/chat-sdk/develop/offline-push/whats-next/ios.mdx b/shared/chat-sdk/develop/offline-push/whats-next/ios.mdx index f13239448..08b178d0f 100644 --- a/shared/chat-sdk/develop/offline-push/whats-next/ios.mdx +++ b/shared/chat-sdk/develop/offline-push/whats-next/ios.mdx @@ -57,9 +57,9 @@ The following code sample shows how to customize the display style in push notif AgoraChatTextMessageBody *body = [[AgoraChatTextMessageBody alloc] initWithText:@"test"]; AgoraChatMessage *message = [[AgoraChatMessage alloc] initWithConversationID:conversationId from:currentUsername to:conversationId body:body ext:nil]; message.ext = @{@"em_apns_ext":@{ - @"em_alert_title": @"customTitle", + @"em_push_title": @"customTitle", @"em_alert_subTitle": @"customSubTitle", - @"em_alert_body": @"customBody" + @"em_push_content": @"customContent" }}; message.chatType = AgoraChatTypeChat; [AgoraChatClient.sharedClient.chatManager sendMessage:message progress:nil completion:nil]; @@ -72,9 +72,9 @@ message.chatType = AgoraChatTypeChat; | `from` | The username of the sender. | | `to` | The username of the receiver. | | `em_apns_ext` | The extension field. | -| `em_alert_title` | The title of push notifications. | +| `em_push_title` | The title of push notifications. | | `em_alert_subTitle` | The subtitle of push notifications. | -| `em_alert_body` | The content of push notifications. | +| `em_push_content` | The content of push notifications. | An example is as follows: diff --git a/shared/chat-sdk/get-started/_enable.mdx b/shared/chat-sdk/get-started/_enable.mdx index bbf8117a8..7894afe72 100644 --- a/shared/chat-sdk/get-started/_enable.mdx +++ b/shared/chat-sdk/get-started/_enable.mdx @@ -1,6 +1,6 @@ import * as data from '@site/data/variables'; -Before using , you need to enable and configure it through [](https://console.agora.io/#onboarding). +Before using , you need to enable and configure it through . ## Prerequisites @@ -9,12 +9,12 @@ To enable , make sure that you have the following: - A valid [ account](/agora-chat/reference/manage-agora-account#create-an-agora-account). - An [ project](/agora-chat/reference/manage-agora-account#create-an-agora-project) that uses **App ID** and **Token** for authentication. -- A pricing plan. For details on how to subscribe, see Subscribe to the pricing plan. +- A pricing plan. For details on how to subscribe, see Subscribe to the pricing plan. ## Enable -1. Log in to the [](https://console.agora.io). +1. Log in to the . 2. In the left navigation bar, click **Project Management** and click **Config** for the project that you want to use. @@ -58,7 +58,7 @@ For details about these advanced features, see the following: Follow these steps to get the project information: -1. Find your -enabled project on the [Project management](https://console.agora.io/projects) page at and click **Config**. +1. Find your -enabled project on the Project Management page on and click **Config**. 2. On the project edit page, find **** and click **Config**. 3. On the config page, get the values of **Data Center**, **AppKey**, **OrgName**, **AppName**, **WebSocket Address**, and **REST API**. @@ -114,7 +114,7 @@ For testing purposes, supports generating temporary tokens fo ## Change the plan -1. Log in to the [](https://console.agora.io). +1. Log in to the . 2. In the left navigation bar, click **Package**. @@ -128,7 +128,7 @@ For testing purposes, supports generating temporary tokens fo ## Disable -1. Log in to the [](https://console.agora.io). +1. Log in to the . 2. In the left navigation bar, click **Project Management** and click **Config** for the project that you want to use. @@ -145,7 +145,7 @@ For testing purposes, supports generating temporary tokens fo Before unsubscribing , disable all projects that have enabled. -1. Log in to the [](https://console.agora.io). +1. Log in to the . 2. In the left navigation bar, click **Package**. diff --git a/shared/chat-sdk/get-started/_get-started-uikit.mdx b/shared/chat-sdk/get-started/_get-started-uikit.mdx index 7ddbd4c10..2728edc80 100644 --- a/shared/chat-sdk/get-started/_get-started-uikit.mdx +++ b/shared/chat-sdk/get-started/_get-started-uikit.mdx @@ -7,32 +7,35 @@ import NextSteps from '@docs/shared/chat-sdk/get-started/get-started-uikit/next- import Reference from '@docs/shared/chat-sdk/get-started/get-started-uikit/reference/index.mdx'; import NoUIKit from '@docs/shared/common/no-uikit-wo-wrappers.mdx'; +Instant messaging connects people wherever they are and allows them to communicate with others in real time. Agora offers an open-source Chat UI Kit project on GitHub. You can clone and run the project or refer to the logic in it to create your own projects. + -Instant messaging connects people wherever they are and allows them to communicate with others in real time. With built-in user interfaces (UI) for the conversation list and contact list, the [ Chat UI Samples](https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-web) enables you to quickly embed real-time messaging into your app without requiring extra effort on the UI. +With built-in user interfaces (UI) for key Chat features, the [ Chat UI Kit](https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-web) enables you to quickly embed real-time messaging into your app without requiring extra effort on the UI. -Instant messaging connects people wherever they are and allows them to communicate with others in real time. With built-in user interfaces (UI) for the conversation list and contact list, the [ Chat UI Samples](https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-android) enables you to quickly embed real-time messaging into your app without requiring extra effort on the UI. +With built-in user interfaces (UI) for key Chat features, the [ Chat UI Kit](https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-android) enables you to quickly embed real-time messaging into your app without requiring extra effort on the UI. -Instant messaging connects people wherever they are and allows them to communicate with others in real time. With built-in user interfaces (UI) for the conversation list and contact list, the [ Chat UI Samples](https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-ios) enables you to quickly embed real-time messaging into your app without requiring extra effort on the UI. +With built-in user interfaces (UI) for key Chat features, the [ Chat UI Kit](https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-ios) enables you to quickly embed real-time messaging into your app without requiring extra effort on the UI. - - - - + +With built-in user interfaces (UI) for key Chat features, the [ Chat UI Kit](https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-flutter) enables you to quickly embed real-time messaging into your app without requiring extra effort on the UI. + + +With built-in user interfaces (UI) for key Chat features, the [ Chat UI Kit](https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-rn) enables you to quickly embed real-time messaging into your app without requiring extra effort on the UI. + + + - - - - + -This page shows a sample code to add peer-to-peer messaging into your app by using the Chat UI Samples. +This page shows sample code to add peer-to-peer messaging into your app by using the Chat UI Kit. ## Understand the tech @@ -44,12 +47,10 @@ The following figure shows the workflow of how clients send and receive peer-to- 2. Client A and Client B log in to Chat. 3. Client A sends a message to Client B. The message is sent to the Chat server, and the server delivers the message to Client B. When Client B receives the message, the SDK triggers an event. Client B listens for the event and gets the message. - ## Prerequisites - - + ## Project setup @@ -57,7 +58,7 @@ The following figure shows the workflow of how clients send and receive peer-to- ## Implementation - + ## Implement peer-to-peer messaging @@ -67,7 +68,7 @@ The following figure shows the workflow of how clients send and receive peer-to- - + ## Next steps @@ -79,7 +80,4 @@ The following figure shows the workflow of how clients send and receive peer-to- - - - \ No newline at end of file diff --git a/shared/chat-sdk/get-started/get-started-sdk/project-setup/android.mdx b/shared/chat-sdk/get-started/get-started-sdk/project-setup/android.mdx index 673ec9b5d..ddfc96b94 100644 --- a/shared/chat-sdk/get-started/get-started-sdk/project-setup/android.mdx +++ b/shared/chat-sdk/get-started/get-started-sdk/project-setup/android.mdx @@ -34,4 +34,13 @@ To integrate into your app, do the following: ``` +1. Prevent code obfuscation. + + In `/Gradle Scripts/proguard-rules.pro`, add the following line: + + ```java + -keep class io.agora.** {*;} + -dontwarn io.agora.** + ``` + diff --git a/shared/chat-sdk/get-started/get-started-sdk/project-setup/flutter.mdx b/shared/chat-sdk/get-started/get-started-sdk/project-setup/flutter.mdx index 6113d72ac..7e2fe5f78 100644 --- a/shared/chat-sdk/get-started/get-started-sdk/project-setup/flutter.mdx +++ b/shared/chat-sdk/get-started/get-started-sdk/project-setup/flutter.mdx @@ -54,15 +54,22 @@ To integrate into your app, do the following: * **Android** - In the `/android/app/build.gradle` file, set the Android `minSdkVersion` to `21` under `defaultConfig {`. + * In the `/android/app/build.gradle` file, set the Android `minSdkVersion` to `21` under `defaultConfig {`. - ``` json - android { - defaultConfig { - minSdkVersion 21 + ``` json + android { + defaultConfig { + minSdkVersion 21 + } } - } - ``` + ``` + + * In the `/android/app/proguard-rules.pro` file, add the following lines to prevent code obfuscation: + + ```bash + -keep class com.hyphenate.** {*;} + -dontwarn com.hyphenate.** + ``` * **iOS** diff --git a/shared/chat-sdk/get-started/get-started-uikit/prerequisites/flutter.mdx b/shared/chat-sdk/get-started/get-started-uikit/prerequisites/flutter.mdx new file mode 100644 index 000000000..1630b5e59 --- /dev/null +++ b/shared/chat-sdk/get-started/get-started-uikit/prerequisites/flutter.mdx @@ -0,0 +1,16 @@ + +- Flutter 2.10 or higher. +- Dart 2.16 or higher. + +- If your target platform is iOS: + - macOS + - Xcode 12.4 or higher with Xcode Command Line Tools + - CocoaPods + - An iOS emulator or a physical iOS device running iOS 10.0 or higher + +- If your target platform is Android: + - macOS or Windows + - Android Studio 4.0 or higher with JDK 1.8 or higher + - An Android emulator or a physical Android device running Android SDK API 21 or higher + + \ No newline at end of file diff --git a/shared/chat-sdk/get-started/get-started-uikit/prerequisites/index.mdx b/shared/chat-sdk/get-started/get-started-uikit/prerequisites/index.mdx index 24fa6e5c5..3cd1835bf 100644 --- a/shared/chat-sdk/get-started/get-started-uikit/prerequisites/index.mdx +++ b/shared/chat-sdk/get-started/get-started-uikit/prerequisites/index.mdx @@ -1,8 +1,12 @@ import Android from './android.mdx'; import Ios from './ios.mdx'; import Web from './web.mdx'; +import Flutter from './flutter.mdx'; +import ReactNative from './react-native.mdx'; + + diff --git a/shared/chat-sdk/get-started/get-started-uikit/prerequisites/ios.mdx b/shared/chat-sdk/get-started/get-started-uikit/prerequisites/ios.mdx index c9c46004d..2c0c97374 100644 --- a/shared/chat-sdk/get-started/get-started-uikit/prerequisites/ios.mdx +++ b/shared/chat-sdk/get-started/get-started-uikit/prerequisites/ios.mdx @@ -1,6 +1,6 @@ -- Xcode, preferrably the latest version. +- Xcode, preferably the latest version. - A simulator or a physical mobile device running iOS 11.0 or later. - CocoaPods. Refer to [Getting Started with CocoaPods](https://guides.cocoapods.org/using/getting-started.html#getting-started) if you have not installed CocoaPods. diff --git a/shared/chat-sdk/get-started/get-started-uikit/prerequisites/react-native.mdx b/shared/chat-sdk/get-started/get-started-uikit/prerequisites/react-native.mdx new file mode 100644 index 000000000..0770df337 --- /dev/null +++ b/shared/chat-sdk/get-started/get-started-uikit/prerequisites/react-native.mdx @@ -0,0 +1,43 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + + + If your target platform is Android: + - macOS 10.15.7 or later, or Windows 10 or later + - Android Studio 2021.3.1 or later, including JDK 1.8 or later + - Visual Studio Code latest + - React Native 0.63.5 or later + - CocoaPods package management tool if your operating system is macOS. + - Powershell 5.1 or later if your operating system is Windows. + - NodeJs 16.18.0 or later, including npm package management tool (brew installation is recommended) + - Typescript 4.0 or above + - Yarn compile and run tool 1.22.19 or later (brew installation is recommended) + - Watchman debugging tool + - npm and related tools + - expo 6.0.0 or above + - A physical or virtual mobile device running Android 6.0 or later + + + If your target platform is iOS: + - macOS 10.15.7 or later + - Xcode 13.4 or later, including command line tools + - Objective-C 2.0 or above (recommended to use the Xcode-included) + - Visual Studio Code latest + - React Native 0.63.5 or later + - NodeJs 16.18.0 or later, including npm package management tool (brew installation is recommended) + - Typescript 4.0 or above + - CocoaPods package management tool + - Yarn compile and run tool 1.22.19 or later (brew installation is recommended) + - Watchman debugging tool + - npm and related tools + - expo 6.0.0 or above + - A physical or virtual mobile device running iOS 10.0 or later + + + +For more information, see [Setting up the environment](https://reactnative.dev/docs/environment-setup). + + \ No newline at end of file diff --git a/shared/chat-sdk/get-started/get-started-uikit/prerequisites/web.mdx b/shared/chat-sdk/get-started/get-started-uikit/prerequisites/web.mdx index 8083e54ff..a53665c13 100644 --- a/shared/chat-sdk/get-started/get-started-uikit/prerequisites/web.mdx +++ b/shared/chat-sdk/get-started/get-started-uikit/prerequisites/web.mdx @@ -7,7 +7,7 @@ - Firefox 10 or later - Chrome 54 or later - Safari 11 or later -- A valid [ account](https://console.agora.io/). +- A valid Agora Account. - An project that has enabled the Chat service. - An [App key](/agora-chat/get-started/enable#get-the-information-of-the-agora-chat-project) and a user token generated on your app server. diff --git a/shared/chat-sdk/get-started/get-started-uikit/project-implementation/flutter.mdx b/shared/chat-sdk/get-started/get-started-uikit/project-implementation/flutter.mdx new file mode 100644 index 000000000..d5a10f573 --- /dev/null +++ b/shared/chat-sdk/get-started/get-started-uikit/project-implementation/flutter.mdx @@ -0,0 +1,410 @@ + + +This section shows how to use the Chat UI Kit to implement peer-to-peer messaging in your app. + +The `agora_chat_uikit` library provides the logic and UI to implement the following Chat functions: + +- Send, receive, and display messages +- Shows the unread message count, and clear messages. Text, image, emoji, file, and audio messages are supported +- Delete conversations and messages +- Customize the UI + +### Chat UI Kit widgets + +
    Do-not-disturb Parameter
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    WidgetFunctionDescription
    `ChatUIKit` The root of all widgets in Chat UI Kit.
    `ChatConversationsView` Conversation list Presents the conversation information, including the user's avatar, nickname, content of the last message, unread message count, and the time when the last message was sent or received.
    Delete conversationDeletes the conversation from the conversation list.
    `ChatMessagesView`Message senderSends text, emoji, image, file, and voice messages.
    Delete messagesDeletes messages.
    Recall messageRecalls message that were sent within 120 seconds.
    Read markYou receive a read receipt after retrieving your message.
    Message sent stateDisplay the status after the message is sent.
    Display messageDisplays one-to-one messages and group messages, including the user's avatar, nickname, the message content, sending or reception time, sending status, and read status. Text, image, emoji, file, voice, and video messages can be displayed.
    + + +Use the following UI Kit widgets to implement Chat functionality with the associated UI in your projects. + +#### `ChatUIKit` + +Place a `ChatUIKit` widget at the top of your widget tree to help refresh the `ChatConversationsView` when returning from `ChatMessagesView`. + + ```dart + ChatUIKit({ + super.key, + this.child, + ChatUIKitTheme? theme, + }); + ``` + +The `ChatUIKit` widget also enables you to customize the theme settings. + +| Property | Description | +| --- | --- | +| `theme` | Chat UI Kit theme for setting component styles. If this property is not set, the default style is used. | + +#### `ChatMessagesView` + +You use `ChatMessagesView` to manage text, images, emojis, files, and voice messages: + +- Send and receive messages +- Delete messages +- Recall messages + +| Property | Description | +| :-------------- | :----- | +| `conversation` | The ChatConversation to which the messages belong. | +| `inputBarTextEditingController` | Text input widget text editing controller. | +| `background` | The background widget.| +| `inputBar` | Text input component. If you don't pass in this property, `ChatInputBar` is used by default.| +| `onTap` | Message bubble click callback.| +| `onBubbleLongPress` | Callback for holding a message bubble.| +| `onBubbleDoubleTap` | Callback for double-clicking a message bubble.| +| `avatarBuilder` | Avatar component builder.| +| `nicknameBuilder` | Nickname component builder.| +| `itemBuilder`| Message bubble. If you don't set this property, the default bubble is used. | +| `moreItems` | Action items displayed after a message bubble is held down. If you return `null` in `onBubbleLongPress`, `moreItems` is used. This property involves three default actions: copy, delete, and recall. | +| `messageListViewController` | Message list controller. You are advised to use the default value. For details, see `ChatMessageListController`. | +| `willSendMessage` | Text message pre-sending callback. This callback needs to return a `ChatMessage` object. | +| `onError`| Error callback, such as no permissions. | +| `enableScrollBar` | Whether to enable the scroll bar. The scroll bar is enabled by default. | +| `needDismissInputWidget` | Callback for dismissing the input widget. If you use a custom input widget, dismiss the input widget when you receive this callback, for example, by calling `FocusNode.unfocus`. See `ChatInputBar`. | +| `inputBarMoreActionsOnTap` | The callback for clicking the plus symbol next to the input box. You need to return the `ChatBottomSheetItems` list. | + + ```dart + ChatMessagesView({ + required this.conversation, + this.inputBarTextEditingController, + this.background, + this.inputBar, + this.onTap, + this.onBubbleLongPress, + this.onBubbleDoubleTap, + this.avatarBuilder, + this.nicknameBuilder, + this.itemBuilder, + this.moreItems, + ChatMessageListController? messageListViewController, + this.willSendMessage, + this.onError, + this.enableScrollBar = true, + this.needDismissInputWidget, + this.inputBarMoreActionsOnTap, + super.key, + }); + ``` + +#### `ChatConversationsView` + +The 'ChatConversationsView' allows you to quickly display and manage the current conversations. + +| Property | Description | +| :-------------- | :----- | +| `controller` | The `ChatConversationsView` controller. | +| `itemBuilder` | Conversation list item builder. Return a widget if you need to customize it. | +| `avatarBuilder` | Avatar builder. If this property is not implemented or you return `null`, the default avatar is used.| +| `nicknameBuilder` | Nickname builder. If you don't set this property or return `null`, the conversation ID is displayed. | +| `onItemTap` | The callback of the click event of the conversation list item. | +| `backgroundWidgetWhenListEmpty` | Background widget when the list is empty. | +| `enablePullReload` | Whether to enable drop-down refresh. | + + + ```dart + ChatConversationsView({ + super.key, + this.onItemTap, + ChatConversationsViewController? controller, + this.itemBuilder, + this.avatarBuilder, + this.nicknameBuilder, + this.backgroundWidgetWhenListEmpty, + this.enablePullReload = false, + this.scrollController, + this.reverse = false, + this.primary, + this.physics, + this.shrinkWrap = false, + this.cacheExtent, + this.dragStartBehavior = DragStartBehavior.down, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + }); + ``` + +### Local integration + +The recommended integration method for the Chat UI Kit is using pub.dev. You can also download the project manually for integration. To do this, open the `uikit_demo` project in your IDE and take the following steps: + +1. Clone the [Chat UIKit for Flutter](https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-flutter) repository. + + ```bash + git clone https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-flutter.git + ``` + +1. Add the dependency: + + ```yaml + dependencies: + agora_chat_uikit: + path: `<#uikit path#>` + ``` + +1. Replace the code in `lib/main.dart` with the following: + + ```dart + import 'package:flutter/material.dart'; + import 'package:agora_chat_uikit/agora_chat_uikit.dart'; + + import 'messages_page.dart'; + + class ChatConfig { + static const String appKey = ""; + static const String userId = ""; + static const String agoraToken = ''; + } + + void main() async { + assert(ChatConfig.appKey.isNotEmpty, + "You need to configure AppKey information first."); + WidgetsFlutterBinding.ensureInitialized(); + final options = ChatOptions( + appKey: ChatConfig.appKey, + autoLogin: false, + ); + await ChatClient.getInstance.init(options); + runApp(const MyApp()); + } + + class MyApp extends StatelessWidget { + const MyApp({super.key}); + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + builder: (context, child) { + // ChatUIKit widget at the top of the widget + return ChatUIKit(child: child!); + }, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: const MyHomePage(title: 'Flutter Demo'), + ); + } + } + + class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + final String title; + + @override + State createState() => _MyHomePageState(); + } + + class _MyHomePageState extends State { + ScrollController scrollController = ScrollController(); + ChatConversation? conversation; + String _chatId = ""; + final List _logText = []; + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Container( + padding: const EdgeInsets.only(left: 10, right: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.max, + children: [ + const SizedBox(height: 10), + const Text("login userId: ${ChatConfig.userId}"), + const Text("agoraToken: ${ChatConfig.agoraToken}"), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + flex: 1, + child: ElevatedButton( + onPressed: () { + _signIn(); + }, + child: const Text("SIGN IN"), + ), + ), + const SizedBox(width: 10), + Expanded( + child: ElevatedButton( + onPressed: () { + _signOut(); + }, + child: const Text("SIGN OUT"), + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: TextField( + decoration: const InputDecoration( + hintText: "Enter recipient's userId", + ), + onChanged: (chatId) => _chatId = chatId, + ), + ), + const SizedBox(height: 10), + ElevatedButton( + onPressed: () { + pushToChatPage(_chatId); + }, + child: const Text("START CHAT"), + ), + ], + ), + Flexible( + child: ListView.builder( + controller: scrollController, + itemBuilder: (_, index) { + return Text(_logText[index]); + }, + itemCount: _logText.length, + ), + ), + ], + ), + ), + ); + } + + void pushToChatPage(String userId) async { + if (userId.isEmpty) { + _addLogToConsole('UserId is null'); + return; + } + if (ChatClient.getInstance.currentUserId == null) { + _addLogToConsole('user not login'); + return; + } + ChatConversation? conv = + await ChatClient.getInstance.chatManager.getConversation(userId); + Future(() { + Navigator.of(context).push(MaterialPageRoute(builder: (_) { + return MessagesPage(conv!); + })); + }); + } + + void _signIn() async { + _addLogToConsole('begin sign in...'); + if (ChatConfig.agoraToken.isNotEmpty) { + try { + await ChatClient.getInstance.loginWithAgoraToken( + ChatConfig.userId, + ChatConfig.agoraToken, + ); + } on ChatError catch (e) { + _addLogToConsole('sign in fail: ${e.description}'); + } + } else { + _addLogToConsole('sign in fail: The agoraToken cannot both be null.'); + } + } + + void _signOut() async { + _addLogToConsole('begin sign out...'); + try { + await ChatClient.getInstance.logout(); + _addLogToConsole('sign out success'); + } on ChatError catch (e) { + _addLogToConsole('sign out fail: ${e.description}'); + } + } + + void _addLogToConsole(String log) { + _logText.add("$_timeString: $log"); + setState(() { + scrollController.jumpTo(scrollController.position.maxScrollExtent); + }); + } + + String get _timeString { + return DateTime.now().toString().split(".").first; + } + } + ``` + +1. To create a `MessagesPage` refer to the following code: + + ```dart + import 'package:agora_chat_uikit/agora_chat_uikit.dart'; + import 'package:flutter/material.dart'; + + class MessagesPage extends StatefulWidget { + const MessagesPage(this.conversation, {super.key}); + + final ChatConversation conversation; + + @override + State createState() => _MessagesPageState(); + } + + class _MessagesPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(widget.conversation.id)), + body: SafeArea( + // Message page in uikit. + child: ChatMessagesView( + conversation: widget.conversation, + ), + ), + ); + } + } + ``` + +
    \ No newline at end of file diff --git a/shared/chat-sdk/get-started/get-started-uikit/project-implementation/index.mdx b/shared/chat-sdk/get-started/get-started-uikit/project-implementation/index.mdx index 24fa6e5c5..a061de27d 100644 --- a/shared/chat-sdk/get-started/get-started-uikit/project-implementation/index.mdx +++ b/shared/chat-sdk/get-started/get-started-uikit/project-implementation/index.mdx @@ -1,8 +1,11 @@ import Android from './android.mdx'; import Ios from './ios.mdx'; import Web from './web.mdx'; +import ReactNative from './react-native.mdx' +import Flutter from './flutter.mdx'; - + + \ No newline at end of file diff --git a/shared/chat-sdk/get-started/get-started-uikit/project-implementation/react-native.mdx b/shared/chat-sdk/get-started/get-started-uikit/project-implementation/react-native.mdx new file mode 100644 index 000000000..f45047194 --- /dev/null +++ b/shared/chat-sdk/get-started/get-started-uikit/project-implementation/react-native.mdx @@ -0,0 +1,47 @@ + + +To initialize the UI Kit, log in to the server, and enter the chat page, take the following steps: + + +1. Initialize the UI Kit + + ```ts + export const App = () => { + return ( + + + + + + + + + ); + }; + ``` + +2. Display Chat details + + ```ts + export function ChatScreen({ + route, + }: NativeStackScreenProps): JSX.Element { + return ( + + + + ); + } + ``` + + \ No newline at end of file diff --git a/shared/chat-sdk/get-started/get-started-uikit/project-setup/android.mdx b/shared/chat-sdk/get-started/get-started-uikit/project-setup/android.mdx index 6cb7e2910..21f7db253 100644 --- a/shared/chat-sdk/get-started/get-started-uikit/project-setup/android.mdx +++ b/shared/chat-sdk/get-started/get-started-uikit/project-setup/android.mdx @@ -3,49 +3,48 @@ Follow the steps to create the environment necessary to add video call into your app. 1. For new projects, in **Android Studio**, create a **Phone and Tablet** [Android project](https://developer.android.com/studio/projects/create-project) with an **Empty Activity**. -
    After creating the project, Android Studio automatically starts gradle sync. Ensure that the sync succeeds before you continue.
    + + After creating the project, Android Studio automatically starts gradle sync. Ensure that the sync succeeds before you continue. 2. Integrate the Chat SDK into your project with Maven Central. - a. In `/Gradle Scripts/build.gradle(Project: )`, add the following lines to add the Maven Central dependency: + 1. In `/Gradle Scripts/build.gradle(Project: )`, add the following lines to add the Maven Central dependency: - ```java - buildscript { - repositories { - ... - mavenCentral() - } - } - allprojects { - repositories { - ... - mavenCentral() - } - } - ``` - -
    After the project is created, Android Studio will automatically start gradle synchronization. Ensure that you proceed to the following operations only after the synchronization is successful.
    - - b. In `/Gradle Scripts/build.gradle(Module: .app)`, add the following lines to integrate the Chat UI Samples into your Android project: + ```java + buildscript { + repositories { + ... + mavenCentral() + } + } + allprojects { + repositories { + ... + mavenCentral() + } + } + ``` + + 1. In `/Gradle Scripts/build.gradle(Module: .app)`, add the following lines to integrate the Chat UI Samples into your Android project: - ```java - android { - defaultConfig { - // The Android OS version should be 21 or higher. - minSdkVersion 21 - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - } - dependencies { - ... - // Replace X.Y.Z with the latest version of the Chat UI Samples. - // For the latest version, go to https://search.maven.org/. - implementation 'io.agora.rtc:chat-uikit:X.Y.Z' - } - ``` + ```java + android { + defaultConfig { + // The Android OS version should be 21 or higher. + minSdkVersion 21 + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + } + dependencies { + ... + // Replace X.Y.Z with the latest version of the Chat UI Samples. + // For the latest version, go to https://search.maven.org/. + implementation 'io.agora.rtc:chat-uikit:X.Y.Z' + } + ``` 3. Add permissions for network and device access. diff --git a/shared/chat-sdk/get-started/get-started-uikit/project-setup/flutter.mdx b/shared/chat-sdk/get-started/get-started-uikit/project-setup/flutter.mdx new file mode 100644 index 000000000..d82abe02d --- /dev/null +++ b/shared/chat-sdk/get-started/get-started-uikit/project-setup/flutter.mdx @@ -0,0 +1,55 @@ + + +Follow these steps to create the environment necessary to integrate Chat UI Kit into your app: + + 1. Create a new project for iOS and Android + + 1. In the terminal, run the following command: + + ```bash + flutter create uikit_demo --platforms=ios,android -i objc -a java + ``` + + 1. Integrate the UI Kit using [pub.dev](https://pub.dev/packages/agora_chat_uikit) (Recommended) + + Execute the following commands in the `uikit_demo` directory: + + ```bash + cd uikit_demo + + flutter pub add agora_chat_uikit + flutter pub get + ``` + + 1. Add permissions for network and device access + + 1. For , in `/app/Manifests/AndroidManifest.xml`, add the following permissions after ``: + + ```xml + + + + + + + + ``` + + 1. For , open **info** in the project navigation panel, and add the following properties to the [Property List](https://help.apple.com/xcode/mac/current/#/dev3f399a2a6): + + | Key | Type | Value | + | --- | --- | --- | + | `Privacy - Microphone Usage Description` | String | For microphone access | + | `Privacy - Camera Usage Description` | String | For camera access | + | `Privacy - Photo Library Usage Description` | String | For photo library access | + + 1. Prevent code obfuscation + + In the `example/android/app/proguard-rules.pro` file, add the following lines: + + ```java + -keep class com.hyphenate.** {*;} + -dontwarn com.hyphenate.** + ``` + + \ No newline at end of file diff --git a/shared/chat-sdk/get-started/get-started-uikit/project-setup/index.mdx b/shared/chat-sdk/get-started/get-started-uikit/project-setup/index.mdx index 499b2b4b9..a061de27d 100644 --- a/shared/chat-sdk/get-started/get-started-uikit/project-setup/index.mdx +++ b/shared/chat-sdk/get-started/get-started-uikit/project-setup/index.mdx @@ -2,8 +2,10 @@ import Android from './android.mdx'; import Ios from './ios.mdx'; import Web from './web.mdx'; import ReactNative from './react-native.mdx' +import Flutter from './flutter.mdx'; - \ No newline at end of file + + \ No newline at end of file diff --git a/shared/chat-sdk/get-started/get-started-uikit/project-setup/react-native.mdx b/shared/chat-sdk/get-started/get-started-uikit/project-setup/react-native.mdx index e2f8372f5..36da2cbb2 100644 --- a/shared/chat-sdk/get-started/get-started-uikit/project-setup/react-native.mdx +++ b/shared/chat-sdk/get-started/get-started-uikit/project-setup/react-native.mdx @@ -1,40 +1,107 @@ -1. **Add the required variables** - In `App.tsx`, add the following to the declarations: +To create a new project using the UI Kit, take the following steps: - ```typescript - // In a production environment, you retrieve the key and salt from - // an authentication server. For this code example you generate locally. - const encryptionKey = ''; - const encryptionSaltBase64 = ''; +1. Create a new React-Native application project + + ```bash + npx react-native init RNUIkitQuickExamle --version 0.71.11 ``` -2. **Import the required modules** +1. Initialize your project + + ```bash + yarn && yarn run env + ``` - In `App.tsx`, add the following import in `import {} from 'react-native-agora-rtc-ng';`: +1. Add dependencies to the project and re-run the `yarn` command - ```typescript - EncryptionMode, + ```json + { + "dependencies": { + "@react-native-async-storage/async-storage": "^1.17.11", + "@react-native-camera-roll/camera-roll": "^5.6.0", + "@react-native-clipboard/clipboard": "^1.11.2", + "@react-native-firebase/app": "^18.0.0", + "@react-native-firebase/messaging": "^18.0.0", + "react-native-audio-recorder-player": "^3.5.3", + "react-native-chat-sdk": "^1.2.0", + "react-native-chat-uikit": "^1.0.0", + "react-native-create-thumbnail": "^1.6.4", + "react-native-document-picker": "^9.0.1", + "react-native-fast-image": "^8.6.3", + "react-native-file-access": "^3.0.4", + "react-native-get-random-values": "~1.8.0", + "react-native-image-picker": "^5.4.2", + "react-native-permissions": "^3.8.0", + "react-native-safe-area-context": "4.5.0", + "react-native-screens": "^3.20.0", + "react-native-video": "^5.2.1" + } + } ``` -3. **Call the channel encryption method to enable channel encryption** +1. Configure the iOS platform. + + Add the following content to the `ios/Podfile` file: - To enable channel encryption in your , you need to: + ```ruby + target 'RNUIkitQuickExamle' do - 1. Create an `EncryptionConfig` instance and specify a configuration for the channel encryption. In the configuration, you specify `encryptionMode`, `encryptionKey`, and `encryptionKdfSalt`. `encryptionKdfSalt` is required for `Aes128Gcm2` and `Aes256Gcm2` modes, it is optional for other encryption modes. + pod 'GoogleUtilities', :modular_headers => true + pod 'FirebaseCore', :modular_headers => true - 2. To enable media encryption, call `enableEncryption` with your `EncryptionConfig` instance. + permissions_path = File.join(File.dirname(`node --print "require.resolve('react-native-permissions/package.json')"`), "ios") + pod 'Permission-Camera', :path => "#{permissions_path}/Camera" + pod 'Permission-MediaLibrary', :path => "#{permissions_path}/MediaLibrary" + pod 'Permission-Microphone', :path => "#{permissions_path}/Microphone" + pod 'Permission-Notifications', :path => "#{permissions_path}/Notifications" + pod 'Permission-PhotoLibrary', :path => "#{permissions_path}/PhotoLibrary" - To implement this logic, in `App.tsx`, add the following code after `agoraEngine.initialize({appId: appID});` + end + ``` - ```typescript - let encryptionConfig = { - encryptionBase64: encryptionBase64, - encryptionKey: encryptionKey, - encryptionMode: EncryptionMode.Aes128Ecb, - }; - engine.enableEncryption(true, encryptionConfig); + Add the following content to the `ios/RNUIkitQuickExamle/Info.plist` file: + + ```xml + + NSCameraUsageDescription + + NSMicrophoneUsageDescription + + NSPhotoLibraryUsageDescription + + ``` - Best practice is to use Aes128Gcm2 or Aes256Gcm2. These modes use salt for higher security. + +1. Configure the Android platform. + + Add the following content to the `android/build.gradle` file: + + ```groovy + buildscript { + ext { + kotlinVersion = '1.6.10' + if (findProperty('android.kotlinVersion')) { + kotlinVersion = findProperty('android.kotlinVersion') + } + } + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + } + } + ``` + + Add the following content to the `android/app/src/main/AndroidManifest.xml` file: + + ```xml + + + + + + + + ``` + \ No newline at end of file diff --git a/shared/chat-sdk/get-started/get-started-uikit/project-test/flutter.mdx b/shared/chat-sdk/get-started/get-started-uikit/project-test/flutter.mdx new file mode 100644 index 000000000..aed0f1402 --- /dev/null +++ b/shared/chat-sdk/get-started/get-started-uikit/project-test/flutter.mdx @@ -0,0 +1,21 @@ + + +To test the UI Kit Chat features example: + +1. In `example/lib/main.dart`, set the `appKey`, `userId`, and `agoraToken` to valid values. + + ```dart + class ChatConfig { + static const String appKey = ""; + static const String userId = ""; + static const String agoraToken = ''; + } + ``` + +1. Click **Sign In**. You see a log message confirming sign-in success. + +1. Run the app on another device or simulator. Ensure that the `userId` values are unique. + +1. On the first device or simulator, enter the user id you just created and click **Start Chat**. You can now start chatting between the two clients. + + \ No newline at end of file diff --git a/shared/chat-sdk/get-started/get-started-uikit/project-test/index.mdx b/shared/chat-sdk/get-started/get-started-uikit/project-test/index.mdx index 51ad8116c..c2a2260d2 100644 --- a/shared/chat-sdk/get-started/get-started-uikit/project-test/index.mdx +++ b/shared/chat-sdk/get-started/get-started-uikit/project-test/index.mdx @@ -2,8 +2,10 @@ import Android from './android.mdx'; import Ios from './ios.mdx'; import Web from './web.mdx'; import ReactNative from './react-native.mdx' +import Flutter from './flutter.mdx'; + diff --git a/shared/chat-sdk/get-started/get-started-uikit/project-test/react-native.mdx b/shared/chat-sdk/get-started/get-started-uikit/project-test/react-native.mdx index 25a8fca94..bff5b97b4 100644 --- a/shared/chat-sdk/get-started/get-started-uikit/project-test/react-native.mdx +++ b/shared/chat-sdk/get-started/get-started-uikit/project-test/react-native.mdx @@ -1,3 +1,56 @@ +To set up and run the example app: + +1. Clone the [ for ](https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-rn) repository. + + ```bash + git clone https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-rn.git + ``` + +1. Initialize the project by running the following commands in a terminal: + + ```bash + yarn && yarn run example-env && yarn run sdk-version + ``` + +1. Configure the necessary parameters. + + 1. In the `example` project, open `example/src/env.ts` and fill in the information. + + ```typescript + export const test = false; // test mode + export const appKey = ''; // from Agora console + export const id = ''; // default user id + export const ps = ''; // default password or token + export const accountType = 'agora'; + ``` + + 1. For the `examples/callkit-example` project, add `appKey` and other values to the `examples/callkit-example/src/env.ts` file. + +1. Configure the FCM file. + + 1. In the `example` project, make the following changes for each platform: + + - For , put the **google-services.json** file under the `examples/android/app` folder. + - For , move **GoogleService-Info.plist** to the `example/iOS/ChatUikitExample` folder. + + 1. In the `examples/callkit-example` project: + + - For , move the file **google-services.json** to the `examples/callkit-example/android/app` folder. + - For , place **GoogleService-Info.plist** under the `examples/callkit-example/iOS/ChatCallkitExample` folder. + +1. Run the sample project + + Execute the following inside a console to compile and run the example app: + + - For : + ```bash + cd example && yarn run Android + ``` + - For : + ```bash + cd example && yarn run pods && yarn run iOS + ``` + \ No newline at end of file diff --git a/shared/chat-sdk/get-started/get-started-uikit/reference/flutter.mdx b/shared/chat-sdk/get-started/get-started-uikit/reference/flutter.mdx new file mode 100644 index 000000000..bcf2feb15 --- /dev/null +++ b/shared/chat-sdk/get-started/get-started-uikit/reference/flutter.mdx @@ -0,0 +1,315 @@ + + +Refer to the fully featured [AgoraChat-UIKit-flutter](https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-flutter) demo app as an implementation reference. To customize the UI Kit, refer to the following information. + +### Customize colors + +You can set the color when adding `ChatUIKit`. + +```dart +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + builder: (context, child) { + // ChatUIKit widget at the top of the widget + return ChatUIKit( + // ChatUIKitTheme + theme: ChatUIKitTheme(), + child: child!, + ); + }, + home: const MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} +``` + +### Add an avatar + +```dart +class _MessagesPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(widget.conversation.id)), + body: SafeArea( + // Message page in uikit. + child: ChatMessagesView( + conversation: widget.conversation, + avatarBuilder: (context, userId) { + // Returns the avatar widget that you want to display. + return Container( + width: 30, + height: 30, + color: Colors.red, + ); + }, + ), + ), + ); + } +} +``` + +### Add a nickname + +```dart +class _MessagesPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(widget.conversation.id)), + body: SafeArea( + // Message page in uikit. + child: ChatMessagesView( + conversation: widget.conversation, + // Returns the nickname widget that you want to display. + nicknameBuilder: (context, userId) { + return Text(userId); + }, + ), + ), + ); + } +} +``` + +### Add the bubble click event + +```dart +class _MessagesPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(widget.conversation.id)), + body: SafeArea( + // Message page in uikit. + child: ChatMessagesView( + conversation: widget.conversation, + // item tap event + onTap: (context, message) { + bubbleClicked(message); + return true; + }, + ), + ), + ); + } + + void bubbleClicked(ChatMessage message) { + debugPrint('bubble clicked'); + } +} +``` + +### Customize the message item widget + +```dart +class _MessagesPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(widget.conversation.id)), + body: SafeArea( + // Message page in uikit. + child: ChatMessagesView( + conversation: widget.conversation, + itemBuilder: (context, model) { + if (model.message.body.type == MessageType.TXT) { + // Custom message bubble + return CustomTextItemWidget( + model: model, + onTap: (context, message) { + bubbleClicked(message); + return true; + }, + ); + } + }, + ), + ), + ); + } + + void bubbleClicked(ChatMessage message) { + debugPrint('bubble clicked'); + } +} + +// Custom message bubble +class CustomTextItemWidget extends ChatMessageListItem { + const CustomTextItemWidget({super.key, required super.model, super.onTap}); + + @override + Widget build(BuildContext context) { + ChatTextMessageBody body = model.message.body as ChatTextMessageBody; + + Widget content = Text( + body.content, + style: const TextStyle( + color: Colors.black, + fontSize: 50, + fontWeight: FontWeight.w400, + ), + ); + return getBubbleWidget(content); + } +} + +``` + +### Customize the input widget + +```dart +class _MessagesPageState extends State { + late ChatMessageListController _msgController; + final TextEditingController _textController = TextEditingController(); + final FocusNode _focusNode = FocusNode(); + @override + void initState() { + super.initState(); + _msgController = ChatMessageListController(widget.conversation); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(widget.conversation.id)), + body: SafeArea( + // Message page in uikit. + child: ChatMessagesView( + conversation: widget.conversation, + messageListViewController: _msgController, + inputBar: customInputWidget(), + needDismissInputWidget: () { + _focusNode.unfocus(); + }, + ), + ), + ); + } + + // custom input widget + Widget customInputWidget() { + return SizedBox( + height: 50, + child: Row( + children: [ + Expanded( + child: TextField( + focusNode: _focusNode, + controller: _textController, + ), + ), + ElevatedButton( + onPressed: () { + final msg = ChatMessage.createTxtSendMessage( + targetId: widget.conversation.id, + content: _textController.text); + _textController.text = ''; + _msgController.sendMessage(msg); + }, + child: const Text('Send')) + ], + ), + ); + } +} + +``` + +### Delete all Messages in the current conversation + +```dart +class _MessagesPageState extends State { + late ChatMessageListController _msgController; + + @override + void initState() { + super.initState(); + _msgController = ChatMessageListController(widget.conversation); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.conversation.id), + actions: [ + TextButton( + onPressed: () { + _msgController.deleteAllMessages(); + }, + child: const Text( + 'Clear', + style: TextStyle(color: Colors.white), + )) + ], + ), + body: SafeArea( + // Message page in uikit. + child: ChatMessagesView( + conversation: widget.conversation, + messageListViewController: _msgController, + ), + ), + ); + } +} +``` + +### Customize actions displayed upon a click of the plus symbol in the page + +```dart +class _MessagesPageState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.conversation.id), + ), + body: SafeArea( + // Message page in uikit. + child: ChatMessagesView( + conversation: widget.conversation, + // Returns a list of custom events + inputBarMoreActionsOnTap: (items) { + ChatBottomSheetItem item = ChatBottomSheetItem( + type: ChatBottomSheetItemType.normal, + onTap: customMoreAction, + label: 'more', + ); + + return items + [item]; + }, + ), + ), + ); + } + + Future customMoreAction() async { + debugPrint('custom action'); + Navigator.of(context).pop(); + } +} +``` + + diff --git a/shared/chat-sdk/get-started/get-started-uikit/reference/index.mdx b/shared/chat-sdk/get-started/get-started-uikit/reference/index.mdx index 51ad8116c..c2a2260d2 100644 --- a/shared/chat-sdk/get-started/get-started-uikit/reference/index.mdx +++ b/shared/chat-sdk/get-started/get-started-uikit/reference/index.mdx @@ -2,8 +2,10 @@ import Android from './android.mdx'; import Ios from './ios.mdx'; import Web from './web.mdx'; import ReactNative from './react-native.mdx' +import Flutter from './flutter.mdx'; + diff --git a/shared/chat-sdk/get-started/get-started-uikit/reference/react-native.mdx b/shared/chat-sdk/get-started/get-started-uikit/reference/react-native.mdx index f5fd3c05b..ea732a930 100644 --- a/shared/chat-sdk/get-started/get-started-uikit/reference/react-native.mdx +++ b/shared/chat-sdk/get-started/get-started-uikit/reference/react-native.mdx @@ -1,8 +1,20 @@ -- *Android* + - 1. Enable the Developer options on your Android device, and then connect it. - 1. Run npx react-native run-android in the project root directory. +- Details -- *iOS:* + - [UIKit](https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-rn/blob/main/packages/react-native-chat-uikit/README.md) + - [UIKit example](https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-rn/blob/main/example/README.md) + + Take a look at this project to experience the fastest and easiest integration: + [Quick Start for UIKit](https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-rn) + +- CallKit Details + + - [CallKit](https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-rn/blob/main/packages/react-native-chat-callkit/README.md) + - [CallKit example](https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-rn/blob/main/examples/callkit-example/README.md) + + Take a look at this project to experience the fastest and easiest integration: + [Quick Start for CallKit](https://github.com/AgoraIO-Usecase/AgoraChat-Callkit-rn) + + - 1. In Xcode, run your project. \ No newline at end of file diff --git a/shared/chat-sdk/hide/_token-server-new.mdx b/shared/chat-sdk/hide/_token-server-new.mdx index 00d9bd7fe..ee1131c81 100644 --- a/shared/chat-sdk/hide/_token-server-new.mdx +++ b/shared/chat-sdk/hide/_token-server-new.mdx @@ -179,7 +179,7 @@ The following figure shows the API call sequence of generating a Chat token with 3. In `/src/main/resource`, create an `application.properties` file to store the information for generating tokens and update it with your project information and token validity period. For example, set `expire.second` as `6000`, which means the token is valid for 6000 seconds. - ``` shellscript + ```shellscript ## Server port. server.port=8090 ## Fill the App ID of your Agora project. diff --git a/shared/chat-sdk/overview/_product-overview.mdx b/shared/chat-sdk/overview/_product-overview.mdx index d0dc20f67..9c0316293 100644 --- a/shared/chat-sdk/overview/_product-overview.mdx +++ b/shared/chat-sdk/overview/_product-overview.mdx @@ -88,7 +88,7 @@ With the engine and algorithms developed by , Chat offers four competitive pricing tiers and transparent billing. You enjoy a generous amount of cost-free usage every month; if your usage exceeds this threshold, you can pay as you go. The more you use, the larger your discount. See [Pricing](/agora-chat/reference/pricing) for details. + Chat offers four competitive pricing tiers and transparent billing. You enjoy a generous amount of cost-free usage every month; if your usage exceeds this threshold, you can pay as you go. The more you use, the larger your discount. See [Pricing](/agora-chat/overview/pricing) for details. ## Supported platforms diff --git a/shared/chat-sdk/reference/_access-token-2.mdx b/shared/chat-sdk/reference/_access-token-2.mdx index 9cb7a1da4..2c8dc0d9a 100644 --- a/shared/chat-sdk/reference/_access-token-2.mdx +++ b/shared/chat-sdk/reference/_access-token-2.mdx @@ -259,7 +259,7 @@ public String buildTokenWithUid(String appId, String appCertificate, String chan | `channelName` | The channel name. The string length must be less than 64 bytes. Supported character scopes are:
    • All lowercase English letters: a to z.
    • All upper English letters: A to Z.
    • All numeric characters: 0 to 9.
    • The space character.
    • Punctuation characters and other symbols, including: "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "\<", "=", ".", ">", "?", "@", "[", "]", "^", "_", " {", "}", "\|", "~", ",".
    | | `uid` | The user ID of the user to be authenticated. A 32-bit unsigned integer with a value range from 1 to (2³² - 1). It must be unique. Set `uid` as 0, if you do not want to authenticate the user ID, that is, any `uid` from the app client can join the channel. | | `role` | The privilege of the user, either as a publisher or a subscriber. This parameter determines whether a user can publish streams in the channel.
    • `ROLE_PUBLISHER(1)`: (default) The user has the privilege of a publisher, that is, the user can publish streams in the channel.
    • `ROLE_SUBSCRIBER(2)`: The user has the privilege of a subscriber, that is, the user can only subscribe to streams, not publish them, in the channel. This value takes effect only if you have enabled co-host authentication. For details, see [Enable co-host authentication](#enable-co-host-authentication).
    | -| `expire` | The Unix timestamp (in seconds) when an AccessToken expires. Set this parameter as the current timestamp plus the valid period of AccessToken2. For example, if you set `expire` as the current timestamp plus 600, the AccessToken2 expires in 10 minutes. An AccessToken2 is valid for 24 hours at most. If you set it to 0 or a period longer than 24 hours, the AccessToken2 is still valid for 24 hours. | +| `expire` | The Unix timestamp (in seconds) when an AccessToken expires. Set this parameter as the current timestamp plus the valid period of AccessToken2. For example, if you set `expire` as the current timestamp plus 600, the AccessToken2 expires in 10 minutes. An AccessToken2 is valid for 24 hours at most. If you set it to a period longer than 24 hours, the AccessToken2 is still valid for 24 hours. | #### API for privilege-level expiration configuration @@ -306,7 +306,7 @@ You need to set an appropriate expiration timestamp. For example, if the expirat Refer to the following steps to enable this function in Agora Console: -1. Log on to [Agora Console](https://console.agora.io/). Under **Projects**, choose a project for which you want to enable co-host authentication, click the **Edit** icon, and enter the **Edit Project** page. +1. Log on to . Under **Projects**, choose a project for which you want to enable co-host authentication, click the **Edit** icon, and enter the **Edit Project** page. 2. In the Features area, click **Enable authentication**. 3. Follow the on-screen instructions to know more about this function, check the box, and click **Enable**. diff --git a/shared/chat-sdk/reference/_callbacks-events.mdx b/shared/chat-sdk/reference/_callbacks-events.mdx index 9ed0ee685..175d46121 100644 --- a/shared/chat-sdk/reference/_callbacks-events.mdx +++ b/shared/chat-sdk/reference/_callbacks-events.mdx @@ -33,7 +33,7 @@ When a user logs in to the Chat app, the Chat server sends a callback to your ap | --- | --- | --- | | `callId` | String | The ID of the callback. The unique identifier assigned to each callback, in the format of `{appKey}_{file_uuid}`, where the value of `file_uuid` is randomly generated. | | `reason` | String | The reason that triggers the callback. `login` indicates that a user logs in to the app. | -| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on [Agora Console](https://console.agora.io/).| +| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on .| | `os` | String | The operating system of the device. Valid values: `ios`, `android`, `linux`, `win`, and `other.` | | `ip` | String | The IP address of the user who logs in to the app. | | `host` | String | The domain name assigned by the Chat service to access RESTful APIs. | @@ -67,7 +67,7 @@ When a user logs out of the Chat app, the Chat server sends a callback to your a | --- | --- | --- | | `callId` | String | The ID of the callback. The unique identifier assigned to each callback, in the format of `{appKey}_{file_uuid}`, where the value of `file_uuid` is randomly generated. | | `reason` | String | The reason that triggers the callback. `logout` indicates that a user logs out of the app. | -| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on [Agora Console](https://console.agora.io/).| +| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on .| | `os` | String | The operating system of the device. Valid values: `ios`, `android`, `linux`, `win`, and `other.` | | `ip` | String | The IP address of the user who logs out of the app. | | `host` | String | The domain name assigned by the Chat service to access RESTful APIs. | @@ -100,7 +100,7 @@ When a user logs out of the Chat app due to being kicked out by another device, | --- | --- | --- | | `callId` | String | The ID of the callback. The unique identifier assigned to each callback, in the format of `{appKey}_{file_uuid}`, where the value of `file_uuid` is randomly generated. | | `reason` | String | The reason that triggers the callback. `replaced` indicates that a user logs out of the app due to being kicked out by another device. | -| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on [Agora Console](https://console.agora.io/).| +| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on .| | `os` | String | The operating system of the device. Valid values: `ios`, `android`, `linux`, `win`, and `other.` | | `ip` | String | The IP address of the user. | | `host` | String | The domain name assigned by the Chat service to access RESTful APIs. | @@ -147,7 +147,7 @@ When a user sends a message in a one-to-one chat, chat group, or chat room of th | `msg_id` | String | The message ID of the callback event. This ID is the same as the `msg_id` when the message is sent. | | `payload` | Object | The structure of the callback event. This field varies according to the type of the message sent in a one-to-one chat, chat group, or chat room. See payload example below for details. | | `securityVersion` | String | This parameter is reserved for future use. | -| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on [Agora Console](https://console.agora.io/).| +| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on .| | `appkey` | String | The key of the app. The unique identifier assigned to each app by the Chat service. | | `host` | String | The domain name assigned by the Chat service to access RESTful APIs. | @@ -403,7 +403,7 @@ When a user recalls a message in a one-to-one chat, chat group, or chat room of | `msg_id` | String | The message ID of the callback event. This ID is the same as the `msg_id` when the end user sends the message. | | `payload` | Object | The structure of the callback event that contains the following fields:
    • `ext`: The message extension. This field is empty when recalling a message.
    • `ack_message_id`: The ID of the message to recall. This ID is the same as `recall_id`.
    • `bodies`: The body of the message callback. This filed is empty when recalling a message.
    | | `securityVersion` | String | This parameter is reserved for future use. | -| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on [Agora Console](https://console.agora.io/).| +| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on .| | `appkey` | String | The key of the app. The unique identifier assigned to each app by the Chat service. | | `host` | String | The domain name assigned by the Chat service to access RESTful APIs. | @@ -442,7 +442,7 @@ When a user performs operations on a chat group or chat room in the Chat app, th | `msg_id` | String | The message ID of the callback event. This ID is the same as the `msg_id` when sending the message. | | `payload` | Object | The content structure of the callback event that contains the following fields:
    • `muc_id`: The unique identifier of the chat group or chat room in the Chat server, in the format of `{appkey}_{group_ID}@conference.easemob.com`.
    • `reason`: Optional. The detailed information about the current operation. See payload example below for details.
    • `is_chatroom`: Whether this event occurs in a chat room.
    • `true`: Yes.
    • `false`: No, this event occurs in a chat group.
    • `operation`: The current operation. See payload example below for details.
    • `status`: The status of the current operation.
    • `description`: The status description.
    • `error_code`: The status code.
    | | `securityVersion` | String | This parameter is reserved for future use. | -| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on [Agora Console](https://console.agora.io/).| +| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on .| | `appkey` | String | The key of the app. The unique identifier assigned to each app by the Chat service. | | `host` | String | The domain name assigned by the Chat service to access RESTful APIs. | @@ -1058,7 +1058,7 @@ When a user performs operations on the contacts in the Chat app, the Chat server | `to` | String | The contact who is operated by the user. | | `msg_id` | String | The message ID of the callback event. This ID is the same as the `msg_id` when sending the message. | | `payload` | Object | The structure of the callback event. See payload example below for details. | -| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on [Agora Console](https://console.agora.io/).| +| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on .| | `appkey` | String | The key of the app. The unique identifier assigned to each app by the Chat service. | | `host` | String | The domain name assigned by the Chat service to access RESTful APIs. | @@ -1199,7 +1199,7 @@ When a user sends an receipt, the Chat server sends a callback to your app serve | :---------- | :------- | :----------------------------------------------------------- | | `chat_type` | String | The type of the event.
    • `read_ack`: Read receipts.
    • `delivery_ack`: Delivery receipts.
    | | `callId` | String | The ID of the callback. The unique identifier assigned to each callback, in the format of `{appKey}_{file_uuid}`, where the value of `file_uuid` is randomly generated. | -| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on [Agora Console](https://console.agora.io/).| +| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on .| | `payload` | Object | The structure of the callback event that contains the following fields:
    • `ext`: The message extension field.
    • `ack_message_id`: The message ID of the receipt callback.
    • `bodies`: The message body.
    | | `host` | String | The domain name assigned by the Chat service to access RESTful APIs. | | `appkey` | String | The key of the app. The unique identifier assigned to each app by the Chat service. | diff --git a/shared/chat-sdk/reference/_chat_receive_webhook.mdx b/shared/chat-sdk/reference/_chat_receive_webhook.mdx index 4971537fa..3e2cc2184 100644 --- a/shared/chat-sdk/reference/_chat_receive_webhook.mdx +++ b/shared/chat-sdk/reference/_chat_receive_webhook.mdx @@ -28,7 +28,7 @@ When a user logs in to the Agora Chat app, the Agora Chat server sends a callbac "ip":"************", "host":"*******", "appkey":"XXXX#XXXX", - "user":"XXXX#XXXXtstXXXX/ios_XXXX01fd-b5a4-84d5-ebeb-bf10XXXX0442", + "user":"XXXX#XXXX_XXXX@easemob.com/ios_XXXX01fd-b5a4-84d5-ebeb-bf10XXXX0442", "version":"3.8.9.1", "timestamp":1642585154644, "status":"online" @@ -38,13 +38,13 @@ When a user logs in to the Agora Chat app, the Agora Chat server sends a callbac | Field | Data Type | Description | | --- | --- | --- | | `callId` | String | The ID of the callback. The unique identifier assigned to each callback, in the format of `{appKey}_{file_uuid}`, where the value of `file_uuid` is randomly generated. | -| `reason` | String | The reason `login` that triggers the callback. | -| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Agora Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on [Agora Console](https://console.agora.io/).| +| `reason` | String | The reason that triggers the callback. `login` indicates that a user logs in to the app. | +| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Agora Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on .| | `os` | String | The operating system of the device. Valid values: `ios`, `android`, `linux`, `win`, and `other.` | | `ip` | String | The IP address of the user who logs in to the app. | | `host` | String | The domain name assigned by the Agora Chat service to access RESTful APIs. | | `appkey` | String | The key of the app. The unique identifier assigned to each app by the Agora Chat service. | -| `user` | String | The ID of the user. The unique identifier of each user in the Agora Chat app, in the format of `{appKey}/{OS}_{deviceId}`. | +| `user` | String | The ID of the login user, in the format of `{app key_username@easemob.com/device operating system_device ID}`, where `@easemob.com` is a fixed string and `device ID` is randomly generated by the SDK. | | `version` | String | The version of the Agora Chat SDK. | | `timestamp` | Long | The Unix timestamp when the Agora Chat server receives the login request, in milliseconds. | | `status` | String | The current status of the user. `online` indicates that the user is online and the app is connected to the Agora Chat server. | @@ -62,7 +62,7 @@ When a user logs out of the Agora Chat app, the Agora Chat server sends a callba "ip":"223.71.97.198:4XXXX", "host":"********", "appkey":"XXXX#XXXX", - "user":"XXXX#XXXXtstXXXX/ios_XXXX0737-db3a-d2b5-da18-b604XXXX195b", + "user":"XXXX#XXXX_XXXX@easemob.com/ios_XXXX0737-db3a-d2b5-da18-b604XXXX195b", "version":"3.8.9.1", "timestamp":1642648914742, "status":"offline" @@ -72,14 +72,13 @@ When a user logs out of the Agora Chat app, the Agora Chat server sends a callba | Field | Data Type | Description | | --- | --- | --- | | `callId` | String | The ID of the callback. The unique identifier assigned to each callback, in the format of `{appKey}_{file_uuid}`, where the value of `file_uuid` is randomly generated. | -| `reason` | String | The reason `logout` that triggers the callback. | -| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Agora Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on [Agora Console](https://console.agora.io/).| +| `reason` | String | The reason that triggers the callback. `logout` indicates that a user logs out of the app. | +| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Agora Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on .| | `os` | String | The operating system of the device. Valid values: `ios`, `android`, `linux`, `win`, and `other.` | | `ip` | String | The IP address of the user who logs out of the app. | | `host` | String | The domain name assigned by the Agora Chat service to access RESTful APIs. | | `appkey` | String | The key of the app. The unique identifier assigned to each app by the Agora Chat service. | -| `user` | String | The ID of the user. The unique identifier of each user in the Agora Chat app, in the format of `{appKey}/{OS}_{deviceId}`. | -| `version` | String | The version of the Agora Chat SDK. | +| `user` | String | The ID of the login user, in the format of `{app key_username@easemob.com/device operating system_device ID}`, where `@easemob.com` is a fixed string and `device ID` is randomly generated by the SDK. | | `timestamp` | Long | The Unix timestamp when the Agora Chat server receives the logout request, in milliseconds. | | `status` | String | The current status of the user. `offline` indicates that the user is offline and the app is disconnected from the Agora Chat server. @@ -95,7 +94,7 @@ When a user is forced by the developer to go offline on the device or due to bei "os":"ios","ip":"223.71.97.198:52709", "host":"msync@ebs-ali-beijing-msync40", "appkey":"XXXX#XXXX", - "user":"XXXX#XXXXtst01XXXX/ios_XXXX01fd-b5a4-84d5-ebeb-bf10XXXX0442", + "user":"XXXX#XXXX_XXXX@easemob.com/ios_XXXX01fd-b5a4-84d5-ebeb-bf10XXXX0442", "version":"3.8.9.1", "timestamp":1642648955563, "status":"offline" @@ -105,13 +104,13 @@ When a user is forced by the developer to go offline on the device or due to bei | Field | Data Type | Description | | --- | --- | --- | | `callId` | String | The ID of the callback. The unique identifier assigned to each callback, in the format of `{appKey}_{file_uuid}`, where the value of `file_uuid` is randomly generated. | -| `reason` | String | The reason `replaced` that triggers the callback. | -| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Agora Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on [Agora Console](https://console.agora.io/).| +| `reason` | String | The reason that triggers the callback. `replaced` indicates that a user logs out of the app due to being kicked out by another device. | +| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Agora Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on .| | `os` | String | The operating system of the device. Valid values: `ios`, `android`, `linux`, `win`, and `other.` | | `ip` | String | The IP address of the user. | | `host` | String | The domain name assigned by the Agora Chat service to access RESTful APIs. | | `appkey` | String | The key of the app. The unique identifier assigned to each app by the Agora Chat service. | -| `user` | String | The ID of the user. The unique identifier of each user in the Agora Chat app, in the format of `{appKey}/{OS}_{deviceId}`. | +| `user` | String | The ID of the login user, in the format of `{app key_username@easemob.com/device operating system_device ID}`, where `@easemob.com` is a fixed string and `device ID` is randomly generated by the SDK. | | `version` | String | The version of the Agora Chat SDK. | | `timestamp` | Long | The Unix timestamp when the Agora Chat server receives the logout request, in milliseconds. | | `status` | String | The current status of the user. `offline` indicates that the app is disconnected from the Agora Chat server and the user is offline. | @@ -153,7 +152,7 @@ When a user sends a message in a one-to-one chat, chat group, or chat room of th | `msg_id` | String | The message ID of the callback event. This ID is the same as the `msg_id` when the message is sent. | | `payload` | Object | The structure of the callback event. This field varies according to the type of the message sent in a one-to-one chat, chat group, or chat room. See payload example below for details. | | `securityVersion` | String | This parameter is reserved for future use. | -| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Agora Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on [Agora Console](https://console.agora.io/).| +| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Agora Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on .| | `appkey` | String | The key of the app. The unique identifier assigned to each app by the Agora Chat service. | | `host` | String | The domain name assigned by the Agora Chat service to access RESTful APIs. | @@ -409,7 +408,7 @@ When a user recalls a message in a one-to-one chat, chat group, or chat room of | `msg_id` | String | The message ID of the callback event. This ID is the same as the `msg_id` when the end user sends the message. | | `payload` | Object | The structure of the callback event that contains the following fields:
    • `ext`: The message extension. This field is empty when recalling a message.
    • `ack_message_id`: The ID of the message to recall. This ID is the same as `recall_id`.
    • `bodies`: The body of the message callback. This filed is empty when recalling a message.
    | | `securityVersion` | String | This parameter is reserved for future use. | -| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Agora Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on [Agora Console](https://console.agora.io/).| +| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Agora Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on .| | `appkey` | String | The key of the app. The unique identifier assigned to each app by the Agora Chat service. | | `host` | String | The domain name assigned by the Agora Chat service to access RESTful APIs. | @@ -448,13 +447,11 @@ When a user performs operations on a chat group or chat room in the Agora Chat a | `msg_id` | String | The message ID of the callback event. This ID is the same as the `msg_id` when sending the message. | | `payload` | Object | The content structure of the callback event that contains the following fields:
    • `muc_id`: The unique identifier of the chat group or chat room in the Agora Chat server, in the format of `{appkey}_{group_ID}@conference.easemob.com`.
    • `reason`: Optional. The detailed information about the current operation. See payload example below for details.
    • `is_chatroom`: Whether this event occurs in a chat room.
      • `true`: Yes.
      • `false`: No, this event occurs in a chat group.
    • `operation`: The current operation. See payload example below for details.
    • `status`: The status of the current operation.
      • `description`: The status description.
      • `error_code`: The status code.
      | | `securityVersion` | String | This parameter is reserved for future use. | -| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Agora Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on [Agora Console](https://console.agora.io/).| +| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Agora Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on .| | `appkey` | String | The key of the app. The unique identifier assigned to each app by the Agora Chat service. | | `host` | String | The domain name assigned by the Agora Chat service to access RESTful APIs. | -### Create a chat group or chat room - -
      This callback is triggered only if the multi-device service is enabled. Once a user creates a chat group or chat room on one device, the Agora Chat server sends callbacks to the other devices, notifying about the creation of the chat group or chat room.
      +### Create a chat group When a user creates a chat group or chat room, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: @@ -666,9 +663,9 @@ When the owner or an admin removes a member from a chat group or chat room, the } ``` -### Add a member to the block list of a chat group or chat room +### Add a member to the block list of a chat group -When the owner or an admin adds a member to the block list of a chat group or chat room, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: +When the owner or an admin adds a member to the block list of a chat group, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: ```json "payload": @@ -676,7 +673,7 @@ When the owner or an admin adds a member to the block list of a chat group or ch "muc_id": "XXXX#XXXX173558358671361@conference.easemob.com", "reason": "", "is_chatroom": false, - // "ban" indicates that the current operation is to add a member to the block list of a chat group or chat room. + // "ban" indicates that the current operation is to add a member to the block list of a chat group. "operation": "ban", "status": { "description": "", @@ -685,9 +682,9 @@ When the owner or an admin adds a member to the block list of a chat group or ch } ``` -### Remove a member from the block list of a chat group or chat room +### Remove a member from the block list of a chat group -When the owner or an admin removes a member from the block list of a chat group or chat room, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: +When the owner or an admin removes a member from the block list of a chat group, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: ```json "payload": @@ -695,7 +692,7 @@ When the owner or an admin removes a member from the block list of a chat group "muc_id": "XXXX#XXXX173549292683265XXXX", "reason": "undefined", "is_chatroom": false, - // "allow" indicates that the current operation is to remove a member from the block list of a chat group or chat room. + // "allow" indicates that the current operation is to remove a member from the block list of a chat group. "operation": "allow", "status": { "description": "", @@ -782,16 +779,16 @@ When the owner or an admin unmutes a member in a chat group or chat room, the Ag } ``` -### Mute a chat group or chat room +### Block a chat group -When a member mutes a chat group or chat room, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: +When a member blocks a chat group, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: ```json "payload": { "muc_id": "XXXX#XXXX173560762007553XXXX", "is_chatroom": false, - // "block" indicates that the current operation is to mute a chat group or chat room. + // "block" indicates that the current operation is to block a chat group or chat room. "operation": "block", "status": { "description": "", @@ -800,16 +797,16 @@ When a member mutes a chat group or chat room, the Agora Chat server sends a cal } ``` -### Unmute a chat group or chat room +### Unblock a chat group -When a member unmutes a chat group or chat room, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: +When a member unblocks a chat group, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: ```json "payload": { "muc_id": "XXXX#XXXX173560762007553XXXX", "is_chatroom": false, - // "unblock" indicates that the current operation is to unmute a chat group or chat room. + // "unblock" indicates that the current operation is to unblock a chat group or chat room. "operation": "unblock", "status": { @@ -819,16 +816,16 @@ When a member unmutes a chat group or chat room, the Agora Chat server sends a c } ``` -### Mute a chat group or chat room globally +### Mute a user in chat groups or chat rooms globally -When a chat group or chat room is globally muted, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: +When a user is muted in all chat groups or chat rooms in the app, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: ```json "payload": { "muc_id": "XXXX#XXXX173553668390913XXXX", "is_chatroom": false, - // "ban_group" indicates that the current operation is to globally mute a chat group or chat room. + // "ban_group" indicates that the current operation is to mute a user in all chat groups or chat rooms in the app. "operation": "ban_group", "status": { @@ -838,10 +835,9 @@ When a chat group or chat room is globally muted, the Agora Chat server sends a } ``` -### Unmute a chat group or chat room globally - -When a chat group or chat room is globally unmuted, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: +### Unmute a user in chat groups or chat rooms globally +When a user is unmuted in all chat groups or chat rooms in the app, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: ```json "payload": { @@ -857,16 +853,15 @@ When a chat group or chat room is globally unmuted, the Agora Chat server sends } ``` -### Transfer the ownership - -When the owner transfers the ownership of a chat group or chat room, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: +### Transfer the group ownership +When the owner transfers the ownership of a chat group, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: ```json "payload": { - "muc_id": "XXXX#XXXX173560762007553XXXX", + "muc_id": "XXXX#XXXX173560762007553XXXX", "is_chatroom": false, - // "assing_owner" indicates that the current operation is to transfer the ownership of a chat group or chat room. + // "assing_owner" indicates that the current operation is to transfer the ownership of a chat group. "operation": "assing_owner", "status": { @@ -933,10 +928,9 @@ When the owner or an admin updates the information of a chat group or chat room, } ``` -### Update the announcements of a chat group or chat room - -When the owner or an admin updates the announcements of a chat group or chat room, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: +### Update the announcement of a chat group or chat room +When the owner or an admin updates the announcement of a chat group or chat room, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: ```json "payload": @@ -955,9 +949,9 @@ When the owner or an admin updates the announcements of a chat group or chat roo } ``` -### Delete the announcements of a chat group or chat room +### Delete the announcement of a chat group or chat room -When the owner or an admin deletes the announcements of a chat group or chat room, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: +When the owner or an admin deletes the announcement of a chat group or chat room, the Agora Chat server sends a callback to your app server. The sample code of the `payload` field is as follows: ```json "payload": @@ -1064,7 +1058,7 @@ When a user performs operations on the contacts in the Agora Chat app, the Agora | `to` | String | The contact who is operated by the user. | | `msg_id` | String | The message ID of the callback event. This ID is the same as the `msg_id` when sending the message. | | `payload` | Object | The structure of the callback event. See payload example below for details. | -| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Agora Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on [Agora Console](https://console.agora.io/).| +| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Agora Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on .| | `appkey` | String | The key of the app. The unique identifier assigned to each app by the Agora Chat service. | | `host` | String | The domain name assigned by the Agora Chat service to access RESTful APIs. | @@ -1177,9 +1171,9 @@ When a user removes a contact from the block list, the Agora Chat server sends a } ``` -## Receipt events +## Read Receipt event -When a user sends an receipt, the Agora Chat server sends a callback to your app server. The sample code is as follows: +When a user sends a read receipt, the Agora Chat server sends a callback to your app server. The sample code is as follows: ```json { @@ -1203,9 +1197,9 @@ When a user sends an receipt, the Agora Chat server sends a callback to your app | Field | Data Type | Description | | :---------- | :------- | :----------------------------------------------------------- | -| `chat_type` | String | The type of the event.
      • `read_ack`: Read receipts.
      • `delivery_ack`: Delivery receipts.
      | +| `chat_type` | String | The event type. The value is `read_ack`, indicating read receipts. | | `callId` | String | The ID of the callback. The unique identifier assigned to each callback, in the format of `{appKey}_{file_uuid}`, where the value of `file_uuid` is randomly generated. | -| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Agora Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on [Agora Console](https://console.agora.io/).| +| `security` | String | The signature in the callback request used to confirm whether this callback is sent from the Agora Chat server. The signature is the MD5 hash of the `{callId} + {secret} + {timestamp}` string, where the value of `secret` can be found on .| | `payload` | Object | The structure of the callback event that contains the following fields:
      • `ext`: The message extension field.
      • `ack_message_id`: The message ID of the receipt callback.
      • `bodies`: The message body.
      | | `host` | String | The domain name assigned by the Agora Chat service to access RESTful APIs. | | `appkey` | String | The key of the app. The unique identifier assigned to each app by the Agora Chat service. | diff --git a/shared/chat-sdk/reference/_chatroom-overview.mdx b/shared/chat-sdk/reference/_chatroom-overview.mdx index 419b89d01..25215aa33 100644 --- a/shared/chat-sdk/reference/_chatroom-overview.mdx +++ b/shared/chat-sdk/reference/_chatroom-overview.mdx @@ -1,6 +1,6 @@ import * as data from '@site/data/variables'; -Chat rooms enable real-time messaging among multiple users and are widely applied in live broadcast use cases as stream chat in Twitch. Chat rooms do not have a strict membership, and members do not retain any permanent relationship with each other. Once going offline, chat room members cannot receive any messages from the chat room and automatically leave the chat room after 2 minutes. If you want to adjust the time, contact [support@agora.io](mailto:support@agora.io). +Chat rooms enable real-time messaging among multiple users and are widely applied in live broadcast use cases as stream chat in Twitch. Chat rooms do not have a strict membership, and members do not retain any permanent relationship with each other. Once going offline, chat room members cannot receive any messages from the chat room and automatically leave the chat room after 2 minutes (members on the chat room allow list remain in the chat room even if they stay offline for 2 minutes or more). If you want to adjust the time, contact [support@agora.io](mailto:support@agora.io). ## Chat room roles and privileges @@ -21,8 +21,8 @@ The following table shows the feature comparisons between a chat group and a cha | Use cases | Group chat scenarios in Signal and Skype, where all members have a stable relationship. | Stream chat scenarios in Twitch, where viewers have no relationship with each other. Once a member quits the stream chat channel, this member leaves the chat room. | | Maximum number of members | 5,000 | 20,000 and more | | Message push support | Members receive push messages when they go offline. | Members do not receive push messages when they go offline. | -| Offline message storage | This feature is supported. The server stores messages sent to offline group members and sends to them once they gets online. A maximum number of 200 messages can be stored for each group conversation. | This feature is disabled for chat rooms by default. To enable it, contact [support@agora.io](mailto:support@agora.io). After this feature is enabled, when a user joins a chat room, the server sends the latest 10 historical messages to the client side via the message receiving callback. The number of historical messages sent to the new chat room member can be increased up to 200. | -| Historical message storage | This feature is supported. You can get historical messages of a conversation from the server. | This feature is not supported for chat rooms. | +| Offline message storage | This feature is supported. Agora Chat servers store messages sent to offline group members and sends to them once they go back online. A maximum number of 200 messages across chat groups can be stored per end user. | Chat room does not store messages sent to offline chat room members and this feature is not supported in chat rooms. | +| Message history | Agora Chat servers store message history, subject to the [data retention period of your package selection](https://docs.agora.io/en/agora-chat/reference/pricing-plan-details?platform=android#message). The history can be retrieved by your app server via [this RESTful API](https://docs.agora.io/en/agora-chat/restful-api/message-management?platform=android#retrieve-historical-messages), in the format of JSON files. You can call [this Client API](https://docs.agora.io/en/agora-chat/client-api/messages/retrieve-messages?platform=android#retrieve-historical-messages-of-the-specified-conversation) to allow the SDK to retrieve message history of a chat group. This allows end users to synchronize messages history across multiple end devices. | Agora Chat servers store message history, subject to the [data retention period of your package selection](https://docs.agora.io/en/agora-chat/reference/pricing-plan-details?platform=android#message). The history can be retrieved by your app server via [this RESTful API](https://docs.agora.io/en/agora-chat/restful-api/message-management?platform=android#retrieve-historical-messages), in the format of JSON files. Agora Chat currently does not support SDK retrieving message history of a chatroom via client APIs. However, when a user joins a chat room, Agora Chat servers send 10 most recent messages to the client side via the message receiving callback. The number of historical messages sent to the new chat room member can be increased up to 200, without additional charges.| | Message reliability | Each member receives all the messages in the chat group. | Members might not see all messages. The SDK discards messages if the chat room message threshold is exceeded. The default threshold is 100 messages per second. You can adjust this threshold according to your needs. | ## Chat room features diff --git a/shared/chat-sdk/reference/_group-overview.mdx b/shared/chat-sdk/reference/_group-overview.mdx index 003f50b49..cd8f6866f 100644 --- a/shared/chat-sdk/reference/_group-overview.mdx +++ b/shared/chat-sdk/reference/_group-overview.mdx @@ -4,9 +4,16 @@ Chat groups are features that enable instant messaging among multiple chat users ## Chat group types -Chat has two types of chat group: -- Public. Public chat groups can be searched, and, depending on the settings of the group, users can join a public chat group directly or with the approval of the group owner or admin. -- Private. Private chat groups cannot be searched, and users cannot join private groups unless they are invited by a group owner or admin. +- Groups can be divided into public groups and private groups according to the public accessibility. + +| Group type | Join method | Retrieval of group information | +| :------- | :---------- | :---------- | +| Public group | Any users can apply to join a group or be invited by the group owner or an admin to join. Whether the join requests need to be approved by the group owner or admins depends on the group setting, for example, `GroupStyle` for Android. |
      • The specifications of a group and the list of public groups are accessible to the public.
      • The group announcement, the list of group shared files, and the group member list are only accessible to users that have joined the group.
      | +| Private group | Users can only be invited to join a group. Whether regular group members, besides the group owner and admins, can invite a user to join the group depends on the group setting, for example, `GroupStyle` for Android.| The group specifications, group announcement, the list of group shared files, and the group member list are only accessible to users that have joined the group. | + +- Groups can be classified into common groups and large groups by size. + - Common group: A group can contain a maximum of 3000 members. + - Large group: A group contains more than 3000 members. This type of group does not support offline push. To create a large group, contact [support@agoro.io](mailto:support@agoro.io). ## Chat group roles and privileges @@ -27,8 +34,8 @@ The specific feature differences are listed in the following table: | Use cases | Group chat scenarios in Signal and Skype, where members have a preexisting relationship with each other. | Stream chat scenarios in Twitch, where viewers have no relationship with each other. Once a member quits the stream channel, they leave the chat room. | | Maximum number of members | 5,000 | 20,000+ | | Message push support | Members receive push messages when they go offline. | Members do not receive push messages when they go offline. | -| Offline message storage | This feature is supported. The server stores messages sent to offline group members and sends to them once they get online. A maximum number of 200 messages can be stored for each group conversation. | This feature is disabled for chat rooms by default. To enable it, contact [support@agora.io](mailto:support@agora.io). After this feature is enabled, when a user joins a chat room, the server sends the latest 10 historical messages to the client side via the message receiving callback. You can contact [support@agora.io](mailto:support@agora.io) to increase the number of historical messages sent to the new chat room member up to 200. | -| Historical message storage | This feature is supported. You can get historical messages of a conversation from the server. | This feature is not supported for chat rooms. | +| Offline message storage | This feature is supported. Agora Chat servers store messages sent to offline group members and sends to them once they go back online. A maximum number of 200 messages across chat groups can be stored per end user. | Chat room does not store messages sent to offline chat room members and this feature is not supported in chat rooms. | +| Message history | Agora Chat servers store message history, subject to the [data retention period of your package selection](https://docs.agora.io/en/agora-chat/reference/pricing-plan-details?platform=android#message). The history can be retrieved by your app server via [this RESTful API](https://docs.agora.io/en/agora-chat/restful-api/message-management?platform=android#retrieve-historical-messages), in the format of JSON files. You can call [this Client API](https://docs.agora.io/en/agora-chat/client-api/messages/retrieve-messages?platform=android#retrieve-historical-messages-of-the-specified-conversation) to allow the SDK to retrieve message history of a chat group. This allows end users to synchronize messages history across multiple end devices. | Agora Chat servers store message history, subject to the [data retention period of your package selection](https://docs.agora.io/en/agora-chat/reference/pricing-plan-details?platform=android#message). The history can be retrieved by your app server via [this RESTful API](https://docs.agora.io/en/agora-chat/restful-api/message-management?platform=android#retrieve-historical-messages), in the format of JSON files. Agora Chat currently does not support SDK retrieving message history of a chatroom via client APIs. However, when a user joins a chat room, Agora Chat servers send 10 most recent messages to the client side via the message receiving callback. The number of historical messages sent to the new chat room member can be increased up to 200, without additional charges.| | Message reliability | Each member receives all the messages in the chat group. | Members might not see all messages. The SDK discards messages if the chat room message threshold is exceeded. The default threshold is 100 messages per second. You can adjust this threshold according to your needs. | diff --git a/shared/chat-sdk/reference/_http-status-codes.mdx b/shared/chat-sdk/reference/_http-status-codes.mdx index 9c8d9131b..44e27dfd4 100644 --- a/shared/chat-sdk/reference/_http-status-codes.mdx +++ b/shared/chat-sdk/reference/_http-status-codes.mdx @@ -102,8 +102,8 @@ This status code indicates that the API request surpasses the call limit. | Status code | Error code | Error message | Description | | :----- | :------------ | :----------------------------------------------------------- | :------------------------------------------------| -| `429` | `resource_limited` | "You have exceeded the limit of the `{pricing_plan} `edition. Please upgrade to higher edition." | The error message returned because the usage of Chat exceeds the limit of the current pricing plan. For details, see [Pricing](pricing). To upgrade the pricing plan, contact support@agora.io. | -| `429` | `reach_limit` | "This request has reached api limit." | The error message returned because the calling frequency of the Chat API surpasses the call limit. For details, see [Limitations](../reference/limitations). To upgrade the pricing plan, contact support@agora.io。 | +| `429` | `resource_limited` | "You have exceeded the limit of the `{pricing_plan} `edition. Please upgrade to higher edition." | The error message returned because the usage of Chat exceeds the limit of the current pricing plan. For details, see [Pricing](../overview/pricing). To upgrade the pricing plan, contact support@agora.io. | +| `429` | `reach_limit` | "This request has reached api limit." | The error message returned because the calling frequency of the Chat API surpasses the call limit. For details, see [Limitations](../reference/limitations). To upgrade the pricing plan, contact support@agora.io. | ## `5xx` - Server error diff --git a/shared/chat-sdk/reference/_ip_whitelist.mdx b/shared/chat-sdk/reference/_ip_whitelist.mdx index 2146982f8..ef1bc8c98 100644 --- a/shared/chat-sdk/reference/_ip_whitelist.mdx +++ b/shared/chat-sdk/reference/_ip_whitelist.mdx @@ -4,7 +4,7 @@ For added security, Agora Chat provides the IP whitelist function. If only certa ### Add an IP address to the IP whitelist -1. Log in to [Agora Console](https://console.agora.io/). +1. Log in to . 2. In the left navigation bar of the Agora console, choose **Chat** > **Features** > **IP Whitelist** to open the **Security Setting** page. @@ -20,7 +20,7 @@ For added security, Agora Chat provides the IP whitelist function. If only certa If you no longer allow an IP address to send messages via the RESTful APIs, you can remove it from the IP whitelist. If you delete all IP addresses in the IP whitelist, that is, the whitelist is empty, all IP addresses can send messages via the RESTful APIs by default. -1. Log in to [Agora Console](https://console.agora.io/). +1. Log in to . 2. In the left navigation bar of the Agora console, choose **Chat** > **Features** > **IP Whitelist** to open the **Security Setting** page. diff --git a/shared/chat-sdk/reference/_limitations.mdx b/shared/chat-sdk/reference/_limitations.mdx index ba4c81b98..fc3437557 100644 --- a/shared/chat-sdk/reference/_limitations.mdx +++ b/shared/chat-sdk/reference/_limitations.mdx @@ -43,6 +43,10 @@ When a group Message attachments like images, voices, videos, and other files can be stored on the server for seven days by default. To raise the limit, you can contact support@agora.io. \ No newline at end of file diff --git a/shared/chat-sdk/reference/_pricing-plan-details.mdx b/shared/chat-sdk/reference/_pricing-plan-details.mdx index 57c375ec2..af74606b1 100644 --- a/shared/chat-sdk/reference/_pricing-plan-details.mdx +++ b/shared/chat-sdk/reference/_pricing-plan-details.mdx @@ -267,4 +267,4 @@ Submit a support ticket if you want to lift the limits and pay for overage charg \* **Peak Concurrent Connections (PCC)** measure the maximum number of TCP long-lived connections made concurrently, to Agora Chat servers, across any given calendar month. This number is typically different from your DAU/MAU. If an end user (with one UID or UUID) logs into Agora Chat, via x number of devices, they are counted as 1 MAU but x PCC. -**PCC overage fee**: We measure the aggregated number of PCCs and MAUs across all your projects at the end of each billing cycle, which ends at every calendar month and charge an overage fee, if applicable. You can find the overage fee schedule by visiting [Agora Console](https://console.agora.io/). Paid packages of Agora Chat by default allow for a PCC smaller or equal to 10% of your total MAU. Exceeding the 10% of MAU won’t impact your end users or disrupt services, as we meter the PCC only at the end of each calendar month. Agora Chat Starter/Pro/Enterprise package subscribers are entitled to at least 500, 1K, and 1K PCC respectively, for free in each month. For example, if a Starter package subscriber has 1K MAU and 500 PCC in a month, there won’t be any PCC overage fee billed. \ No newline at end of file +**PCC overage fee**: We measure the aggregated number of PCCs and MAUs across all your projects at the end of each billing cycle, which ends at every calendar month and charge an overage fee, if applicable. You can find the overage fee schedule by visiting . Paid packages of Agora Chat by default allow for a PCC smaller or equal to 10% of your total MAU. Exceeding the 10% of MAU won’t impact your end users or disrupt services, as we meter the PCC only at the end of each calendar month. Agora Chat Starter/Pro/Enterprise package subscribers are entitled to at least 500, 1K, and 1K PCC respectively, for free in each month. For example, if a Starter package subscriber has 1K MAU and 500 PCC in a month, there won’t be any PCC overage fee billed. \ No newline at end of file diff --git a/shared/chat-sdk/reference/_pricing.mdx b/shared/chat-sdk/reference/_pricing.mdx index 6458941d8..67670e4be 100644 --- a/shared/chat-sdk/reference/_pricing.mdx +++ b/shared/chat-sdk/reference/_pricing.mdx @@ -6,7 +6,7 @@ Note that if you have already signed a contract with Agora, the billing terms an ## Overview -Each month, Agora calculates your bill for using Chat, [issues your bill, and deducts your fee](billing-policies). If you subscribe, cancel, or switch to another plan, your fee is prorated for the current month. If your account is [suspended](billing-policies), Agora stores your app data for six months. Agora suggests you top up your account in a timely fashion or export the data before it is deleted. +Each month, Agora calculates your bill for using Chat, [issues your bill, and deducts your fee](../reference/billing-policies). If you subscribe, cancel, or switch to another plan, your fee is prorated for the current month. If your account is [suspended](../reference/billing-policies), Agora stores your app data for six months. Agora suggests you top up your account in a timely fashion or export the data before it is deleted. ## Composition @@ -79,8 +79,8 @@ Chat provides the translation and content moderation features to meet your busin Before using Chat, refer to the following steps to subscribe to a plan: -1. Log in to [Agora Console](https://console.agora.io/), on the left navigation bar, click **Package** > **Chat** > **Subscribe** on the left navigation bar. -2. Check [pricing plan details](pricing-plan-details), choose the plan you want to use, click **Subscribe**, and make the payment. +1. Log in to , on the left navigation bar, click **Package** > **Chat** > **Subscribe** on the left navigation bar. +2. Check [pricing plan details](../reference/pricing-plan-details), choose the plan you want to use, click **Subscribe**, and make the payment. Subscription takes effect immediately. After subscribing to a plan, you can click **Package** > **Chat** > **Manage**, and on this page you can click **Show More** to view your subscription details. diff --git a/shared/chat-sdk/reference/_security-best-practice.mdx b/shared/chat-sdk/reference/_security-best-practice.mdx index 10ab5f1ac..d59e250f2 100644 --- a/shared/chat-sdk/reference/_security-best-practice.mdx +++ b/shared/chat-sdk/reference/_security-best-practice.mdx @@ -57,7 +57,7 @@ Once selected, the data center cannot be changed. Chat does not support data mig ## Security best practice checklist Use this list to quickly check what protection measures you need to take to ensure the security of your app and users: -1. Enable token authentication on [Agora Console](https://console.agora.io/). +1. Enable token authentication on . 2. Disable **No certificate** on your project management page so that your app authenticates users with tokens only. 3. [Deploy a token server](../develop/authentication#deploy-a-token-server) in your backend services. 4. Protect the token server by only allowing the app's backend server to connect to the token server. diff --git a/shared/chat-sdk/reference/error-codes/android.mdx b/shared/chat-sdk/reference/error-codes/android.mdx index 64a77c1d5..65fb33d2e 100644 --- a/shared/chat-sdk/reference/error-codes/android.mdx +++ b/shared/chat-sdk/reference/error-codes/android.mdx @@ -28,14 +28,14 @@ The error codes and error messages might be returned in the following ways: | 204 | `USER_NOT_FOUND` | The user does not exist. For example, the user ID specified for login or retrieval of the conversation list of a user does not exist. | | 205 | `USER_ILLEGAL_ARGUMENT` | Incorrect user parameter. For example, the user ID specified during user account registration or user attribute updating is invalid or empty. | | 206 | `USER_LOGIN_ANOTHER_DEVICE` | The user has logged in on another device. If the multi-device service is not enabled, the user is forced to log out of the current device upon successful login on another device with the same account. | -| 207 | `USER_REMOVED` | The user account is deleted on the [Agora Console](https://console.agora.io/). | +| 207 | `USER_REMOVED` | The user account is deleted on the . | | 209 | `PUSH_UPDATECONFIGS_FAILED` | Fails to update the push configurations. For example, a failure of changing the nickname displayed for message push or updating do-not-disturb settings. | | 210 | `USER_PERMISSION_DENIED` | The user has no permission to perform this operation. For example, a user that is banned attempts to send a message. | | 213 | `USER_BIND_ANOTHER_DEVICE` | The user has logged in to another device. This error occurs when a user has logged in on one device and try to log in on another one with the same account, if the app is configured to persist the login state on the current device while preventing the login on another device in a single-device login scenario. This error code is deprecated. | | 214 | `USER_LOGIN_TOO_MANY_DEVICES` | The user has reached the maximum number of devices on which he or she can log in with the same user account. This error occurs on a device with auto login enabled in a multi-device login scenario if the app turns on the switch of login on another device never kicking the user off on a current login device when the maximum number of login devices is exceeded. For example, a user can log in on at most four devices. At first, the user stays logged in on four devices, A (with auto login enabled), B, C, D. Then, the user gets logged out on device A due to network disruption and logs in on device E manually. After the network is available, auto login is performed on device A and fails due to the limit of login devices, triggering this error. | | 215 | `USER_MUTED` | The user is muted and not allowed to send messages in a chat group or chat room. | | 216 | `USER_KICKED_BY_CHANGE_PASSWORD` | The user has changed the login password. Once the login password is changed, the current login session ends and the user must log in again with the new password. | -| 217 | `USER_KICKED_BY_OTHER_DEVICE` | The user gets kicked off from a device in a multi-device login scenario on the [Agora Console](https://console.agora.io/) or by calling an API on another device. | +| 217 | `USER_KICKED_BY_OTHER_DEVICE` | The user gets kicked off from a device in a multi-device login scenario on the or by calling an API on another device. | | 218 | `USER_ALREADY_LOGIN_ANOTHER` | Another user has logged in. This error occurs if a user attempts to log in on the same device with another account before logging out on the device with the current account. | | 219 | `USER_MUTED_BY_ADMIN` | The user is muted globally and cannot send a message. | | 220 | `USER_DEVICE_CHANGED` | The device on which a user attempts to log in is not the last one in a single-device login scenario. By default, if a user logs in on a another device, he or she will get kicked off from the current device in a single-device login scenario. This error occurs if the app turns on the switch of keeping the logged-in state on the current device if a user attempts to log in on another device. | @@ -57,7 +57,7 @@ The error codes and error messages might be returned in the following ways: | 501 | `MESSAGE_INCLUDE_ILLEGAL_CONTENT` | The message contains inappropriate content. This error occurs when a message is found by the filtering system to contain inappropriate content. | | 502 | `MESSAGE_SEND_TRAFFIC_LIMIT` | Messages are too large or sent too frequently. It is recommended that the user reduce the message sending frequency or the message size. | | 504 | `MESSAGE_RECALL_TIME_LIMIT` | The message recall timeout. This error occurs when a message fails to be recalled because the timeout period expires. | -| 505 | `SERVICE_NOT_ENABLED` | The feature that the user is attempting to use is not enabled. This feature needs to be enabled on the [Agora Console](https://console.agora.io/) or by contacting [support@agora.io](mailto:support@agora.io) before it is ready to use. | +| 505 | `SERVICE_NOT_ENABLED` | The feature that the user is attempting to use is not enabled. This feature needs to be enabled on the or by contacting [support@agora.io](mailto:support@agora.io) before it is ready to use. | | 506 | `MESSAGE_EXPIRED` | The period for sending the read receipt for a group message has expired. The default validity period is 3 days. | | 507 | `MESSAGE_ILLEGAL_WHITELIST` | The user is not on the allow list of a group or chat room. This error occurs when a user that is not on the allow list attempts to send a message in a chat group or chat room if all members of the group or chat room are muted. | | 508 | `MESSAGE_EXTERNAL_LOGIC_BLOCKED` | During the pre-sending callback, the message that the user is attempting to send is blocked by a message filtering rule defined in the app server. | @@ -90,7 +90,7 @@ The error codes and error messages might be returned in the following ways: | 1100 | `PRESENCE_PARAM_LENGTH_EXCEED` | The length of a parameter in the presence-related API call exceeds the upper limit. | | 1101 | `PRESENCE_CANNOT_SUBSCRIBE_YOURSELF` | The user cannot subscribe to her or his own presence status. | | 1110 | `TRANSLATE_PARAM_INVALID` | Translation parameter error. | -| 1111 | `TRANSLATE_SERVICE_NOT_ENABLE` | The translation service has not been enabled. The translation service needs to be enabled on the [Agora Console](https://console.agora.io/) before it is ready to use. | +| 1111 | `TRANSLATE_SERVICE_NOT_ENABLE` | The translation service has not been enabled. The translation service needs to be enabled on the before it is ready to use. | | 1112 | `TRANSLATE_USAGE_LIMIT` | The translation usage has reached the upper limit. | | 1113 | `TRANSLATE_MESSAGE_FAIL` | Fails to translate the message. | | 1200 | `MODERATION_FAILED` | The message moderation result provided by the third-party content moderation service is `Rejected`. | diff --git a/shared/chat-sdk/reference/error-codes/ios.mdx b/shared/chat-sdk/reference/error-codes/ios.mdx index ab894fc98..51064d122 100644 --- a/shared/chat-sdk/reference/error-codes/ios.mdx +++ b/shared/chat-sdk/reference/error-codes/ios.mdx @@ -25,14 +25,14 @@ During the run time of the Chat SDK, if the method call succeeds, the SDK return | 204 | `AgoraChatErrorUserNotFound` | The user does not exist. For example, the user ID specified for login or retrieval of the conversation list of a user does not exist. | | 205 | `AgoraChatErrorUserIllegalArgument` | Incorrect user parameter. For example, the user ID specified during user account registration or user attribute updating is invalid or empty. | | 206 | `AgoraChatErrorUserLoginOnAnotherDevice` | The user has logged in on another device. If the multi-device service is not enabled, the user is forced to log out of the current device upon successful login on another device with the same account. | -| 207 | `AgoraChatErrorUserRemoved` | The user account is deleted on the [Agora Console](https://console.agora.io/).| +| 207 | `AgoraChatErrorUserRemoved` | The user account is deleted on the .| | 209 | `AgoraChatErrorUpdateApnsConfigsFailed` | Fails to update the push configurations. For example, a failure of changing the nickname displayed for message push or updating do-not-disturb settings. | | 210 | `AgoraChatErrorUserPermissionDenied` | The user has no permission to perform this operation. For example, a user that is banned attempts to send a message. | | 213 | `AgoraChatErrorUserBindAnotherDevice` | The user has logged in to another device. This error occurs when a user has logged in on one device and try to log in on another one with the same account, if the app is configured to persist the login state on the current device while preventing the login on another device in a single-device login scenario. This error code is deprecated. | | 214 | `AgoraChatErrorUserLoginTooManyDevices` | The user has reached the maximum number of devices on which he or she can log in with the same user account. This error occurs on a device with auto login enabled in a multi-device login scenario if the app turns on the switch of login on another device never kicking the user off on a current login device when the maximum number of login devices is exceeded. For example, a user can log in on at most four devices. At first, the user stays logged in on four devices, A (with auto login enabled), B, C, D. Then, the user gets logged out on device A due to network disruption and logs in on device E manually. After the network is available, auto login is performed on device A and fails due to the limit of login devices, triggering this error. | | 215 | `AgoraChatErrorUserMuted` | The user is muted and not allowed to send messages in a chat group or chat room. | | 216 | `AgoraChatErrorUserKickedByChangePassword` | The user has changed the login password. Once the login password is changed, the current login session ends and the user must log in again with the new password. | -| 217 | `AgoraChatErrorUserKickedByOtherDevice` | The user gets kicked off from a device in a multi-device login scenario on the [Agora Console](https://console.agora.io/) or by calling an API on another device. | +| 217 | `AgoraChatErrorUserKickedByOtherDevice` | The user gets kicked off from a device in a multi-device login scenario on the or by calling an API on another device. | | 218 | `AgoraChatErrorUserAlreadyLoginAnother` | Another user has logged in. This error occurs if a user attempts to log in on the same device with another account before logging out on the device with the current account. | | 219 | `AgoraChatErrorUserMutedByAdmin` | The user is muted globally and cannot send a message. | | 220 | `AgoraChatErrorUserDeviceChanged` | The device on which a user attempts to log in is not the last one in a single-device login scenario. By default, if a user logs in on a another device, he or she will get kicked off from the current device in a single-device login scenario. This error occurs if the app turns on the switch of keeping the logged-in state on the current device if a user attempts to log in on another device. | @@ -53,7 +53,7 @@ During the run time of the Chat SDK, if the method call succeeds, the SDK return | 500 | `AgoraChatErrorMessageInvalid` | The message is invalid. For example, during message sending, the message object or message ID is empty, or the user ID of the message sender is inconsistent with the user ID of the current login session. | | 502 | `AgoraChatErrorMessageTrafficLimit` | Messages are too large or sent too frequently. It is recommended that the user reduce the message sending frequency or the message size. | | 504 | `AgoraChatErrorMessageRecallTimeLimit` | The message recall timeout. This error occurs when a message fails to be recalled because the timeout period expires. | -| 505 | `AgoraChatErrorServiceNotEnable` | The feature that the user is attempting to use is not enabled. This feature needs to be enabled on the [Agora Console](https://console.agora.io/) or by contacting [support@agora.io](mailto:support@agora.io) before it is ready to use. | +| 505 | `AgoraChatErrorServiceNotEnable` | The feature that the user is attempting to use is not enabled. This feature needs to be enabled on the or by contacting [support@agora.io](mailto:support@agora.io) before it is ready to use. | | 506 | `AgoraChatErrorMessageExpired` | The period for sending the read receipt for a group message has expired. The default validity period is 3 days. | | 507 | `AgoraChatErrorMessageIllegalWhiteList` | The user is not on the allow list of a group or chat room. This error occurs when a user that is not on the allow list attempts to send a message in a chat group or chat room if all members of the group or chat room are muted. | | 508 | `AgoraChatErrorMessageExternalLogicBlocked` | During the pre-sending callback, the message that the user is attempting to send is blocked by a message filtering rule defined in the app server. | @@ -86,7 +86,7 @@ During the run time of the Chat SDK, if the method call succeeds, the SDK return | 1100 | `AgoraChatErrorPresenceParamExceed` | The length of a parameter in the presence-related API call exceeds the upper limit. | | 1101 | `AgoraChatErrorPresenceCannotSubscribeSelf` | The user cannot subscribe to her or his own presence status. | | 1110 | `AgoraChatErrorTranslateParamError` | Translation parameter error. | -| 1111 | `AgoraChatErrorTranslateServiceNotEnabled` | The translation service has not been enabled. The translation service needs to be enabled on the [Agora Console](https://console.agora.io/) before it is ready to use. | +| 1111 | `AgoraChatErrorTranslateServiceNotEnabled` | The translation service has not been enabled. The translation service needs to be enabled on the before it is ready to use. | | 1112 | `AgoraChatErrorTranslateUsageLimit` | The translation usage has reached the upper limit. | | 1113 | `AgoraChatErrorTranslateServiceFail` | Fails to translate the message. | | 1200 | `AgoraChatErrorModerationFailed` | The message moderation result provided by the third-party content moderation service is `Rejected`.| diff --git a/shared/chat-sdk/reference/error-codes/web.mdx b/shared/chat-sdk/reference/error-codes/web.mdx index d2f347840..4001d86e4 100644 --- a/shared/chat-sdk/reference/error-codes/web.mdx +++ b/shared/chat-sdk/reference/error-codes/web.mdx @@ -34,9 +34,9 @@ During the run time of the Chat SDK, the error codes and error messages might be | 204 | `USER_NOT_FOUND` | The user is not found. For example, the user invited to join a chat group during group creation does not exist. | | 205 | `MESSAGE_PARAMETER_ERROR` | Message parameter error. For example, the message ID is not set during message recall or the user ID of the message recipient is not passed in during message sending. | | 206 | `WEBIM_CONNCTION_USER_LOGIN_ANOTHER_DEVICE` | The user has logged in on another device. If the multi-device service is not enabled, the user is forced to log out of the current device upon successful login on another device with the same account. | -| 207 | `WEBIM_CONNCTION_USER_REMOVED` | The user account is deleted on the [Agora Console](https://console.agora.io/). | +| 207 | `WEBIM_CONNCTION_USER_REMOVED` | The user account is deleted on the . | | 216 | `WEBIM_CONNCTION_USER_KICKED_BY_CHANGE_PASSWORD` | The user has changed the login password. Once the login password is changed, the current login session ends and the user must log in again with the new password. | -| 217 | `WEBIM_CONNCTION_USER_KICKED_BY_OTHER_DEVICE` | The user gets kicked off from a device in a multi-device login scenario on the [Agora Console](https://console.agora.io/) or by calling an API on another device.| +| 217 | `WEBIM_CONNCTION_USER_KICKED_BY_OTHER_DEVICE` | The user gets kicked off from a device in a multi-device login scenario on the or by calling an API on another device.| | 219 | `USER_MUTED_BY_ADMIN` | The user is muted globally and cannot send a message. | | 221 | `USER_NOT_FRIEND` | The user cannot send a message to another user that is not on the contact list. This error occurs if the switch of allowing message sending only between contacts is turned on. | | 500 | `SERVER_BUSY` | The server is busy. | @@ -44,7 +44,7 @@ During the run time of the Chat SDK, the error codes and error messages might be | 502 | `MESSAGE_EXTERNAL_LOGIC_BLOCKED` | The message is intercepted. This error occurs when a message is intercepted by the anti-spam service.| | 503 | `SERVER_UNKNOWN_ERROR` | The SDK fails to send the message due to an unknown error. | | 504 | `MESSAGE_RECALL_TIME_LIMIT` | The message recall timeout. This error occurs when a message fails to be recalled because the timeout period expires. | -| 505 | `SERVICE_NOT_ENABLED` | The feature that the user is attempting to use is not enabled. This feature needs to be enabled on the [Agora Console](https://console.agora.io/) or by contacting [support@agora.io](mailto:support@agora.io) before it is ready to use. | +| 505 | `SERVICE_NOT_ENABLED` | The feature that the user is attempting to use is not enabled. This feature needs to be enabled on the or by contacting [support@agora.io](mailto:support@agora.io) before it is ready to use. | | 506 | `SERVICE_NOT_ALLOW_MESSAGING` | The user is not on the allow list of a group or chat room. This error occurs when a user that is not on the allow list attempts to send a message in a chat group or chat room if all members of the group or chat room are muted. | | 507 | `SERVICE_NOT_ALLOW_MESSAGING_MUTE` | The user is muted and not allowed to send messages in a chat group or chat room. | | 508 | `MESSAGE_MODERATION_BLOCKED` | The message moderation result provided by the third-party content moderation service is `Rejected`. | diff --git a/shared/chat-sdk/reference/error-codes/windows.mdx b/shared/chat-sdk/reference/error-codes/windows.mdx index 453af61e9..b533371f8 100644 --- a/shared/chat-sdk/reference/error-codes/windows.mdx +++ b/shared/chat-sdk/reference/error-codes/windows.mdx @@ -46,13 +46,13 @@ SDKClient.Instance.Login(username, passwd, | 204 | `USER_NOT_FOUND` | The user does not exist. For example, the user ID specified for login or retrieval of the conversation list of a user does not exist. | | 205 | `USER_ILLEGAL_ARGUMENT` | Incorrect user parameter. For example, the user ID specified during user account registration or user attribute updating is invalid or empty. | | 206 | `USER_LOGIN_ANOTHER_DEVICE` | The user has logged in on another device. If the multi-device service is not enabled, the user is forced to log out of the current device upon successful login on another device with the same account. | -| 207 | `USER_REMOVED` | The user account is deleted on the [Agora Console](https://console.agora.io/). | +| 207 | `USER_REMOVED` | The user account is deleted on the . | | 210 | `USER_PERMISSION_DENIED` | The user has no permission to perform this operation. For example, a user that is banned attempts to send a message. | | 213 | `USER_BIND_ANOTHER_DEVICE` | The user has logged in to another device. This error occurs when a user has logged in on one device and try to log in on another one with the same account, if the app is configured to persist the login state on the current device while preventing the login on another device in a single-device login scenario. This error code is deprecated.| | 214 | `USER_LOGIN_TOO_MANY_DEVICES` | The user has reached the maximum number of devices on which he or she can log in with the same user account. This error occurs on a device with auto login enabled in a multi-device login scenario if the app turns on the switch of login on another device never kicking the user off on a current login device when the maximum number of login devices is exceeded. For example, a user can log in on at most four devices. At first, the user stays logged in on four devices, A (with auto login enabled), B, C, D. Then, the user gets logged out on device A due to network disruption and logs in on device E manually. After the network is available, auto login is performed on device A and fails due to the limit of login devices, triggering this error. | | 215 | `USER_MUTED` | The user is muted and not allowed to send messages in a chat group or chat room. | | 216 | `USER_KICKED_BY_CHANGE_PASSWORD` | The user has changed the login password. Once the login password is changed, the current login session ends and the user must log in again with the new password. | -| 217 | `USER_KICKED_BY_OTHER_DEVICE` | In a multi-device login scenario, the user gets kicked off from a device in on the [Agora Console](https://console.agora.io/) or by calling an API on another device. | +| 217 | `USER_KICKED_BY_OTHER_DEVICE` | In a multi-device login scenario, the user gets kicked off from a device in on the or by calling an API on another device. | | 218 | `USER_ALREADY_LOGIN_ANOTHER` | Another user has logged in. This error occurs if a user attempts to log in on the same device with another account before logging out on the device with the current account. | | 219 | `USER_MUTED_BY_ADMIN` | The user is muted globally and cannot send a message. | | 220 | `USER_DEVICE_CHANGED` | The device on which a user attempts to log in is not the last one in a single-device login scenario. By default, if a user logs in on a another device, he or she will get kicked off from the current device in a single-device login scenario. This error occurs if the app turns on the switch of keeping the logged-in state on the current device if a user attempts to log in on another device. | @@ -74,7 +74,7 @@ SDKClient.Instance.Login(username, passwd, | 501 | `MESSAGE_INCLUDE_ILLEGAL_CONTENT` | The message contains inappropriate content. This error occurs when a message is found by the filtering system to contain inappropriate content. | | 502 | `MESSAGE_SEND_TRAFFIC_LIMIT` | Messages are too large or sent too frequently. It is recommended that the user reduce the message sending frequency or the message size. | | 504 | `MESSAGE_RECALL_TIME_LIMIT` | The message recall timeout. This error occurs when a message fails to be recalled because the timeout period expires. | -| 505 | `SERVICE_NOT_ENABLED` | The feature that the user is attempting to use is not enabled. This feature needs to be enabled on the [Agora Console](https://console.agora.io/) or by contacting [support@agora.io](mailto:support@agora.io) before it is ready to use. | +| 505 | `SERVICE_NOT_ENABLED` | The feature that the user is attempting to use is not enabled. This feature needs to be enabled on the or by contacting [support@agora.io](mailto:support@agora.io) before it is ready to use. | | 506 | `MESSAGE_EXPIRED` | The period for sending the read receipt for a group message has expired. The default validity period is 3 days. | | 507 | `MESSAGE_ILLEGAL_WHITELIST` | The user is not on the allow list of a group or chat room. This error occurs when a user that is not on the allow list attempts to send a message in a chat group or chat room if all members of the group or chat room are muted. | | 508 | `MESSAGE_EXTERNAL_LOGIC_BLOCKED` | During the pre-sending callback, the message that the user is attempting to send is blocked by a message filtering rule defined in the app server. | @@ -107,7 +107,7 @@ SDKClient.Instance.Login(username, passwd, | 1100 | `PRESENCE_PARAM_LENGTH_EXCEED` | The length of a parameter in the presence-related API call exceeds the upper limit. | | 1101 | `PRESENCE_CANNOT_SUBSCRIBE_YOURSELF` | The user cannot subscribe to her or his own presence status. | | 1110 | `TRANSLATE_PARAM_INVALID` | Translation parameter error. | -| 1111 | `TRANSLATE_SERVICE_NOT_ENABLE` | The translation service has not been enabled. The translation service needs to be enabled on the [Agora Console](https://console.agora.io/) before it is ready to use. | +| 1111 | `TRANSLATE_SERVICE_NOT_ENABLE` | The translation service has not been enabled. The translation service needs to be enabled on the before it is ready to use. | | 1112 | `TRANSLATE_USAGE_LIMIT` | The translation usage has reached the upper limit. | | 1113 | `TRANSLATE_MESSAGE_FAIL` | Fails to translate the message. | | 1200 | `THIRD_MODERATION_FAILED` | The message moderation result provided by the third-party content moderation service is `Rejected`. | diff --git a/shared/chat-sdk/reference/release-notes/ios.mdx b/shared/chat-sdk/reference/release-notes/ios.mdx index 90987d7a6..3089a94d9 100644 --- a/shared/chat-sdk/reference/release-notes/ios.mdx +++ b/shared/chat-sdk/reference/release-notes/ios.mdx @@ -16,7 +16,7 @@ v1.2.0 was released on December 6, 2023. - Added the function of pinning a conversation: - `AgoraChatManager#pinConversation`: Pins a conversation. - `AgoraChatManager#getPinnedConversationsFromServerWithCursor`: Retrieves the pinned conversations from the server. -- Added the `AgoraChatManager#getConversationsFromServerWithCursor` method to retrieve the conversation list from the server. +- Added the `AgoraChatManager#getConversationsFromServerWithCursor` method to retrieve the conversation list from the server. Marked `getConversationsFromServer` and `getConversationsFromServerByPage:pageSize:completion:` deprecated. - Added the `AgoraChatManager#getAllConversations:` method to retrieve local conversations in the reverse chronological order of when conversations are active. - Added `AgoraChatFetchServerMessagesOption` as the parameter configuration class for retrieving historical messages from the server. - Added the `AgoraChatManager#fetchMessagesFromServerBy` method to retrieve historical messages of a conversation from the server according to `AgoraChatFetchServerMessagesOption`, the parameter configuration class for retrieving historical messages. diff --git a/shared/chat-sdk/reference/release-notes/react-native.mdx b/shared/chat-sdk/reference/release-notes/react-native.mdx index 41a884bcf..53a674c56 100644 --- a/shared/chat-sdk/reference/release-notes/react-native.mdx +++ b/shared/chat-sdk/reference/release-notes/react-native.mdx @@ -1,5 +1,13 @@ +## v1.2.1 + +v1.2.1 was released on February 2, 2024. + +#### Improvements + +- Updated native Android dependencies to support Android 14 (API34). + ## v1.2.0 v1.2.0 was released on December 6, 2023. @@ -17,8 +25,7 @@ v1.2.0 was released on December 6, 2023. - Added the function of pinning a conversation: - `ChatManager.pinConversation`: Pins a conversation. - `ChatManager.fetchPinnedConversationsFromServerWithCursor`: Retrieves a list of pinned conversations from the server. -- Added the `ChatManager.fetchConversationsFromServerWithCursor` method to retrieve the conversation list from the server. - Marked the `ChatManager.fetchAllConversations` method deprecated. +- Marked the `ChatManager.fetchAllConversations` method deprecated. - Added the `ChatManager.fetchHistoryMessagesByOptions` method to retrieve historical messages of a conversation from the server according to `ChatFetchMessageOptions`, the parameter configuration class for pulling historical messages. - Added `ChatFetchMessageOptions` as the parameter configuration class for pulling historical messages from the server. - Added the function of managing custom attributes of group members: diff --git a/shared/chat-sdk/reference/release-notes/unity.mdx b/shared/chat-sdk/reference/release-notes/unity.mdx index f0b88d153..b0747a297 100644 --- a/shared/chat-sdk/reference/release-notes/unity.mdx +++ b/shared/chat-sdk/reference/release-notes/unity.mdx @@ -1,5 +1,14 @@ +## v1.2.1 + +v1.2.1 was released on February 8, 2024. + +#### Improvements + +- Marked `DeInit` function as obsolete. +- Added support for the iOS platform setting in the `UNITY_EDITOR` mode. + ## v1.2.0 v1.2.0 was released on December 6, 2023. @@ -64,6 +73,15 @@ v1.2.0 was released on December 6, 2023. - The underlying resources are not released when the SDK is not initialized. - The issues in user attribute updates on Android and iOS systems. +## v1.1.4 + +v1.1.4 was released on February 8, 2024. + +#### Improvements + +- Marked `DeInit` function as obsolete. +- Added support for the iOS platform setting in the `UNITY_EDITOR` mode. + ## v1.1.3 v1.1.3 was released on October 2, 2023. diff --git a/shared/chat-sdk/reference/release-notes/web.mdx b/shared/chat-sdk/reference/release-notes/web.mdx index 62c18e480..8870a8c68 100644 --- a/shared/chat-sdk/reference/release-notes/web.mdx +++ b/shared/chat-sdk/reference/release-notes/web.mdx @@ -16,7 +16,7 @@ v1.2.0 was released on December 6, 2023. - Added the function of pinning a conversation: - `pinConversation`: Pins a conversation. - `getServerPinnedConversations`: Retrieves the pinned conversations from the server. -- Added the `getServerConversations` method to retrieve the conversation list from the server. +- Added the `getServerConversations` method to retrieve the conversation list from the server. Marked `getConversationlist` deprecated. - Added the `searchOptions` parameter object to the `getHistoryMessages` method for pulling historical messages from the server. - Added the function of managing custom attributes of group members: - `setGroupMemberAttributes`: Sets custom attributes of a group member. diff --git a/shared/chat-sdk/restful-api/_message-management.mdx b/shared/chat-sdk/restful-api/_message-management.mdx index 77bd6f64e..8ef97da6c 100644 --- a/shared/chat-sdk/restful-api/_message-management.mdx +++ b/shared/chat-sdk/restful-api/_message-management.mdx @@ -94,7 +94,7 @@ The request body is a JSON object, which contains the following parameters: | `sync_device` | Bool | Whether to synchronize the message to the message sender.
      • `true`: Yes.
      • `false`: No.
      | No | | `routetype` | String | The route type when the message is not online.
      • To send the message only when the user is online, set this parameter as `ROUTE_TYPE`.
      • To send the message regardless of whether the user is online or not, do not set this parameter.
      | No | | `ext` | JSON | The extension field of the message. It cannot be `null`.| No | -| `ext.em_ignore_notification` | Whether to send a silent message:
      • `true`: Yes;
      • (Default)`false`: No.
      Sending silent messages means that when the user is offline, Agora Chat will not push message notifications to the user's device through a third-party message push service. Therefore, users will not receive push notifications for messages. When the user goes online again, all messages sent from the offline period will be received. Unlike the Do Not Disturb mode which is set by the recipient to prevent notifications during a certain period, sending silent messages is set by the sender.| Boolean | +| `ext.em_ignore_notification` | Bool | Whether to send a silent message:
      • `true`: Yes;
      • (Default)`false`: No.
      Sending silent messages means that when the user is offline, Agora Chat will not push message notifications to the user's device through a third-party message push service. Therefore, users will not receive push notifications for messages. When the user goes online again, all messages sent from the offline period will be received. Unlike the Do Not Disturb mode which is set by the recipient to prevent notifications during a certain period, sending silent messages is set by the sender.| No | @@ -110,28 +110,29 @@ The request body is a JSON object, which contains the following parameters: | Parameter | Type | Description | Required | | --- | --- | --- | --- | - | `filename` | String | The name of the image file. | Yes | - | `secret` | String | The secret for accessing the image file. You can obtain the value of `secret` from the `share-secret` parameter in the response body of the [upload](#upload) method. If you set `resctrict-access` as `true` in the request header of `upload` when uploading the image file, ensure that you set this parameter. | No | - | `size` | JSON | The size of the image (in pixels). This parameter contains two fields:
      • height: The image height.
      • width: The image width.
      | Yes | + | `filename` | String | The name of the image file. You are advised to pass in this parameter, or there is no image name displayed on the client that receives the message. | No | + | `secret` | String | The secret for accessing the image file. You can obtain the value of `secret` from the `share-secret` parameter in the response body of the [upload](#upload) method. If you set `restrict-access` as `true` in the request header of `upload` when uploading the image file, ensure that you set this parameter. | No | + | `size` | JSON | The size of the image (in pixels). This parameter contains two fields:
      • height: The image height.
      • width: The image width.
      | No | | `url` | String | The URL address of the image file, in the format of `https://{host}/{org_name}/{app_name}/chatfiles/{file_uuid}`, in which `file_uuid` can be obtained from the response body of `upload` after you upload the file to the server. | Yes | - Voice message | Parameter | Type | Description | Required | | --- | --- | --- | --- | - | `filename` | String | The name of the audio file. | Yes | - | `secret` | String | The secret for accessing the audio file. You can obtain the value of `secret` from the `share-secret` parameter in the response body of the [upload](#upload) method. If you set `resctrict-access` as `true` in the request header of `upload` when uploading the audio file, ensure that you set this parameter. | No | - | `length` | Int | The length of the audio file (in seconds). | Yes | + | `filename` | String | The name of the audio file. You are advised to pass in this parameter, or there is no voice file name displayed on the client that receives the message. | No | + | `secret` | String | The secret for accessing the audio file. You can obtain the value of `secret` from the `share-secret` parameter in the response body of the [upload](#upload) method. If you set `restrict-access` as `true` in the request header of `upload` when uploading the audio file, ensure that you set this parameter. | No | + | `length` | Int | The length of the audio file (in seconds). | No | | `url` | String | The URL address of the audio file, in the format of `https://{host}/{org_name}/{app_name}/chatfiles/{file_uuid}`, in which `file_uuid` can be obtained from the response body of `upload` after you upload the file to the server. | Yes | - Video message | Parameter | Type | Description | Required | | --- | --- | --- | --- | - | `thumbnail` | String | The URL address of the video thumbnail, in the format of `https://{host}/{org_name}/{app_name}/chatfiles/{file_uuid}`, in which `file_uuid` can be obtained from the response body of `upload` after you upload the file to the server. | Yes | - | `length` | Int | The length of the video file (in seconds). | Yes | - | `secret` | String | The secret for accessing the video file. You can obtain the value of `secret` from the `share-secret` parameter in the response body of the [upload](#upload) method. If you set `resctrict-access` as `true` in the request header of `upload` when uploading the video file, ensure that you set this parameter. | No | - | `file_length` | Long | The data length of the video file (in bytes). | Yes | + | `filename` | String | The name of the video file. You are advised to pass in this parameter, or there is no video file name displayed on the client that receives the message. | No | + | `thumb` | String | The URL address of the video thumbnail, in the format of `https://{host}/{org_name}/{app_name}/chatfiles/{file_uuid}`, in which `file_uuid` can be obtained from the response body of `upload` after you upload the file to the server. | No | + | `length` | Int | The length of the video file (in seconds). | No | + | `secret` | String | The secret for accessing the video file. You can obtain the value of `secret` from the `share-secret` parameter in the response body of the [upload](#upload) method. If you set `restrict-access` as `true` in the request header of `upload` when uploading the video file, ensure that you set this parameter. | No | + | `file_length` | Long | The data length of the video file (in bytes). | No | | `thumb_secret` | String | The secret for accessing the video thumbnail. You can obtain the value of `thumb_secret` from the `share-secret` parameter in the response body of the [upload](#upload) method. If you set `restrict-access` as `true` in the request header of `upload` when uploading the thumbnail, ensure that you set this parameter. | No | | `url` | String | The URL address of the video file, in the format of `https://{host}/{org_name}/{app_name}/chatfiles/{file_uuid}`, in which `file_uuid` can be obtained from the response body of `upload` after you upload the file to the server. | Yes | @@ -139,7 +140,7 @@ The request body is a JSON object, which contains the following parameters: | Parameter | Type | Description | Required | | --- | --- | --- | --- | - | `filename` | String | The name of the file. | Yes | + | `filename` | String | The name of the file. You are advised to pass in this parameter, or there is no file name displayed on the client that receives the message. | Yes | | `secret` | String | The secret for accessing the file. You can obtain the value of `secret` from the `share-secret` parameter in the response body of the [upload](#upload) method. If you set `restrict-access` as `true` in the request header of `upload` when uploading file, ensure that you set this parameter. | No | | `url` | String | The URL address of the file, in the format of `https://{host}/{org_name}/{app_name}/chatfiles/{file_uuid}`, in which `file_uuid` can be obtained from the response body of `upload` after you upload the file to the server. | Yes | @@ -163,8 +164,6 @@ The request body is a JSON object, which contains the following parameters: | --- | --- | --- | --- | | `customEvent` | String | The event type customized by the user. The value of this parameter should be a regular expression, for example, `[a-zA-Z0-9-_/\.]{1,32}`. | No | | `customExts` | JSON | The event attribute customized by the user. The data type is `Map`. You can set a maximum of 16 elements. | No | - | `from` | String | The username of the message sender. If you do not set this field, the Chat server takes the `admin` as the sender. If you set it as the empty string "", this request fails. | No | - | `ext` | JSON | The extension property, which supports customized settings of the app. Do not set it as `ext:null`. | No | #### HTTP response @@ -215,8 +214,7 @@ If the returned HTTP status code is not `200`, the request fails. You can refer ```shell # Replace {YourToken} with the app token generated on your server - curl -X POST -i 'https://XXXX/XXXX/XXXX/messages/users' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Authorization: Bearer {YourToken}' -d '{"from": "user1","to": ["user2"],"type": "video","body": {"thumb" : "https://XXXX/XXXX/XXXX/chatfiles/67279b20-7f69-11e4-8eee-21d3334b3a97","length" : 0,"secret":"VfXXXXNb_","file_length" : 58103,"thumb_secret" : "ZyXXXX2I","url" : "https://XXXX/XXXX/XXXX/chatfiles/671dfe30-XXXX-XXXX-ba67-8fef0d502f46"}}' - ``` + curl -X POST -i 'https://XXXX/XXXX/XXXX/messages/users' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Authorization: Bearer {YourToken}' -d '{"from": "user1","to": ["user2"],"type": "video","body": {"filename":"testvideo.avi","thumb" : "https://XXXX/XXXX/XXXX/chatfiles/67279b20-7f69-11e4-8eee-21d3334b3a97","length" : 0,"secret":"VfXXXXNb_","file_length" : 58103,"thumb_secret" : "ZyXXXX2I","url" : "https://XXXX/XXXX/XXXX/chatfiles/671dfe30-XXXX-XXXX-ba67-8fef0d502f46"}}' ``` - Send a file message @@ -239,6 +237,27 @@ If the returned HTTP status code is not `200`, the request fails. You can refer curl -X POST -i "https://XXXX/XXXX/XXXX/messages/users" -H 'Content-Type: application/json' -H 'Accept: application/json' -H "Authorization:Bearer {YourToken}" -d '{"from": "user1","to": ["user2"],"type": "cmd","body":{"action":"action1"}}' ``` + - Send a custom message + + ```shell + # Replace {YourToken} with the app token generated on your server + curl -X POST -i "https://XXXX/XXXX/XXXX/messages/users" \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H "Authorization:Bearer " \ + -d '{ + "from": "user1", + "to": ["user2"], + "type": "custom", + "body": { + "customEvent": "custom_event" + "customExts":{ + "ext_key1":"ext_value1" + } + } + }' + ``` + ##### Response example - Send a text message @@ -466,7 +485,7 @@ If the returned HTTP status code is not `200`, the request fails. You can refer ```shell # Replace {YourToken} with the app token generated on your server - curl -X POST -i 'https://XXXX/XXXX/XXXX/messages/chatgroups' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Authorization: Bearer {YourToken}' -d '{"from": "user1","to": ["184524748161025"],"type": "video","body": {"thumb" : "https://XXXX/XXXX/XXXX/chatfiles/67279b20-7f69-11e4-8eee-21d3334b3a97","length" : 0,"secret":"VfXXXXNb_","file_length" : 58103,"thumb_secret" : "ZyXXXX2I","url" : "https://XXXX/XXXX/XXXX/chatfiles/671dfe30-XXXX-XXXX-ba67-8fef0d502f46"}}' + curl -X POST -i 'https://XXXX/XXXX/XXXX/messages/chatgroups' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Authorization: Bearer {YourToken}' -d '{"from": "user1","to": ["184524748161025"],"type": "video","body": {"filename" : "testvideo.avi","thumb" : "https://XXXX/XXXX/XXXX/chatfiles/67279b20-7f69-11e4-8eee-21d3334b3a97","length" : 0,"secret":"VfXXXXNb_","file_length" : 58103,"thumb_secret" : "ZyXXXX2I","url" : "https://XXXX/XXXX/XXXX/chatfiles/671dfe30-XXXX-XXXX-ba67-8fef0d502f46"}}' ``` - Send a file message @@ -490,6 +509,27 @@ If the returned HTTP status code is not `200`, the request fails. You can refer curl -X POST -i "https://XXXX/XXXX/XXXX/messages/chatgroups" -H 'Content-Type: application/json' -H 'Accept: application/json' -H "Authorization:Bearer {YourToken}" -d '{"from": "user1","to": ["184524748161025"],"type": "cmd","body":{"action":"action1"}}' ``` +- Send a Custom message + + ```shell + # Replace {YourToken} with the app token generated on your server + curl -X POST -i "https://XXXX/XXXX/XXXX/messages/chatgroups" \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H "Authorization:Bearer " \ + -d '{ + "from": "user1", + "to": ["184524748161025"], + "type": "custom", + "body": { + "customEvent": "custom_event" + "customExts":{ + "ext_key1":"ext_value1" + } + } + }' + ``` + ##### Response example - Send a text message @@ -677,7 +717,7 @@ The other parameters and descriptions are the same with those of [Sending a one- #### HTTP response -##### Reponse body +##### Response body If the returned HTTP status code is `200`, the request succeeds, and the response body contains the following parameters: @@ -725,7 +765,7 @@ If the returned HTTP status code is not `200`, the request fails. You can refer ```shell # Replace {YourToken} with the app token generated on your server - curl -X POST -i 'https://XXXX/XXXX/XXXX/messages/chatrooms' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Authorization: Bearer {YourToken}' -d '{"from": "user1","to": ["185145305923585"],"type": "video","body": {"thumb" : "https://XXXX/XXXX/XXXX/chatfiles/67279b20-7f69-11e4-8eee-21d3334b3a97","length" : 0,"secret":"VfXXXXNb_","file_length" : 58103,"thumb_secret" : "ZyXXXX2I","url" : "https://XXXX/XXXX/XXXX/chatfiles/671dfe30-XXXX-XXXX-ba67-8fef0d502f46"}}' + curl -X POST -i 'https://XXXX/XXXX/XXXX/messages/chatrooms' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Authorization: Bearer {YourToken}' -d '{"from": "user1","to": ["185145305923585"],"type": "video","body": {"filename":"testvideo.avi","thumb" : "https://XXXX/XXXX/XXXX/chatfiles/67279b20-7f69-11e4-8eee-21d3334b3a97","length" : 0,"secret":"VfXXXXNb_","file_length" : 58103,"thumb_secret" : "ZyXXXX2I","url" : "https://XXXX/XXXX/XXXX/chatfiles/671dfe30-XXXX-XXXX-ba67-8fef0d502f46"}}' ``` - Send a file message @@ -749,6 +789,27 @@ If the returned HTTP status code is not `200`, the request fails. You can refer curl -X POST -i "https://XXXX/XXXX/XXXX/messages/chatrooms" -H 'Content-Type: application/json' -H 'Accept: application/json' -H "Authorization:Bearer {YourToken}" -d '{"from": "user1","to": ["185145305923585"],"type": "cmd","body":{"action":"action1"}}' ``` +- Send a custom message + + ```shell + # Replace {YourToken} with the app token generated on your server + curl -X POST -i "https://XXXX/XXXX/XXXX/messages/chatrooms" \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H "Authorization:Bearer " \ + -d '{ + "from": "user1", + "to": ["185145305923585"], + "type": "custom", + "body": { + "customEvent": "custom_event" + "customExts":{ + "ext_key1":"ext_value1" + } + } + }' + ``` + ##### Response example - Send a text message @@ -769,7 +830,7 @@ If the returned HTTP status code is not `200`, the request fails. You can refer } ``` -- Sned an image message +- Send an image message ```json { @@ -1471,14 +1532,11 @@ For the parameters and detailed descriptions, see [Common parameters](#param). | Parameter | Type | Required | Description | | :---------- | :------- | :------- | :----------------------------------------------------------- | -| `msg_id` | String | Yes | The ID of the recalled message. | -| `to` | String | No | The user, chat group, or chat room that receives the recalled message. You can specify a username, a chat group ID, or a chat room ID.
      **Note**: If the recalled message exceeds the message storage duration and no longer exists in the server, this message cannot be recalled on the server side. You can only recall the message on the receiver client instead. | -| `chat_type` | String | Yes | The type of the chat where the recalled message is located.
      • `chat`: An one-on-one chat.
      • `groupchat`: A chat group.
      • `chatroom`: A chat room.
      | | `msg_id` | String | Yes | The ID of the message to recall. As only one message can be recalled each time, you can pass in only one message ID. | -| `to` | String | No | The user, chat group, or chat room that receives the message to recall. You can specify a user ID, a chat group ID, or a chat room ID. **Note**: If the message to recall no longer exists on the server, only the message on the recipient client is recalled. | +| `to` | String | Yes | The user, chat group, or chat room that receives the message to recall. You can specify a user ID, a chat group ID, or a chat room ID. **Note**: If the message to recall no longer exists on the server, only the message on the recipient client is recalled. | | `chat_type` | String | Yes | The type of the chat where the message to recall is sent.
      • `chat`: An one-on-one chat.
      • `groupchat`: A chat group.
      • `chatroom`: A chat room.
      | | `from` | String | No | The user who recalls the message. By default, the recaller is the app admin. You can also specify another user as the recaller. | -| `force` | bool | Yes | Whether to allow to recall messages beyond the storage time on the server. For details on the message storage duration on the server, see [Message storage duration](https://docs.agora.io/en/agora-chat/reference/limitations#message-storage-duration).
      • `true`: Yes. In this case, you can recall messages within the recall period or those beyond the storage time on the server. For the latter, this API recalls the messages locally saved by the recipient. If the message sending time is between your recall duration and the storage duration on the server, the recall fails. For example, if the recall duration is 2 minutes and the storage time on the server is 7 days, you can recall a message sent within 2 minutes or one that was sent more than 7 days ago; if the message is sent 3 minutes ago, the recall will fail.
      • `false`: No. You cannot recall messages beyond the storage time on the server. If you use the default recall time of 2 minutes or a custom duration, the server can only recall the messages sent within the specified time, and those beyond this time cannot be recalled. For example, if you set the recall time to 3 minutes and the message is sent 4 minutes ago, the recall will fail.
      | +| `force` | bool | No | Whether to allow to recall messages beyond the storage time on the server. For details on the message storage duration on the server, see [Message storage duration](https://docs.agora.io/en/agora-chat/reference/limitations#message-storage-duration).
      • `true`: Yes. In this case, you can recall messages within the recall period or those beyond the storage time on the server. For the latter, this API recalls the messages locally saved by the recipient. If the message sending time is between your recall duration and the storage duration on the server, the recall fails. For example, if the recall duration is 2 minutes and the storage time on the server is 7 days, you can recall a message sent within 2 minutes or one that was sent more than 7 days ago; if the message is sent 3 minutes ago, the recall will fail.
      • `false`: No. You cannot recall messages beyond the storage time on the server. If you use the default recall time of 2 minutes or a custom duration, the server can only recall the messages sent within the specified time, and those beyond this time cannot be recalled. For example, if you set the recall time to 3 minutes and the message is sent 4 minutes ago, the recall will fail.
      | ### HTTP response @@ -1666,7 +1724,7 @@ POST https://{host}/{orgName}/{appName}/messages/users/import | `body` | JSON | The message content. For different message types, this parameter contains different fields. For details, see [Body of different message types](#body). | Yes | | `is_ack_read` | Bool | Whether to set the message as read.
      • `true`: Yes.
      • `false`: No.
      | No | | `msg_timestamp` | Long | The timestamp for importing the messages, in milliseconds. If you leave this parameter empty, the server automatically sets it as the current time. | No | -| `need_download` | Bool | Whether to download the attachment and upload it to the server:
      • `true`: Yes.
      • `false`: (Default) No.
      | No | +| `need_download` | Bool | Whether to download the attachment and upload it to the server:
      • `true`: Yes. In this case, you need to make sure that the attachment URL is publicly accessible.
      • `false`: (Default) No.
      | No | ### HTTP Response @@ -1769,7 +1827,7 @@ POST https://{host}/{orgName}/{appName}/messages/chatgroups/import | `body` | JSON | The message content. For different message types, this parameter contains different fields. For details, see [Body of different message types](#body). | Yes | | `is_ack_read` | Bool | Whether to set the message as read.
      • `true`: Yes.
      • `false`: No.
      | No | | `msg_timestamp` | Long | The timestamp for importing the messages, in milliseconds. If you leave this parameter empty, the server automatically sets it as the current time. | No | -| `need_download` | Bool | Whether to download the attachment and upload it to the server:
      • `true`: Yes.
      • `false`: (Default) No.
      | No | +| `need_download` | Bool | Whether to download the attachment and upload it to the server:
      • `true`: Yes. In this case, you need to make sure that the attachment URL is publicly accessible.
      • `false`: (Default) No.
      | No | ### HTTP Response diff --git a/shared/chat-sdk/restful-api/_offline-push-configuration.mdx b/shared/chat-sdk/restful-api/_offline-push-configuration.mdx index e8c8107fb..af9acc6b5 100644 --- a/shared/chat-sdk/restful-api/_offline-push-configuration.mdx +++ b/shared/chat-sdk/restful-api/_offline-push-configuration.mdx @@ -12,7 +12,7 @@ This page shows how to call Chat RESTful APIs to set the display name, display s Before calling the following methods, ensure that you meet the following: - You understand the call frequency limit of the Chat RESTful APIs as described in Limitations. -- You have activated the advanced features for push in [Agora Console](https://console.agora.io/). Advanced features allow you to set the push notification mode, do-not-disturb mode, and custom push template. +- You have activated the advanced features for push in . Advanced features allow you to set the push notification mode, do-not-disturb mode, and custom push template.
      You must contact support@agora.io to disable the advanced features for push as this operation will delete all the relevant configurations.
      diff --git a/shared/chat-sdk/restful-api/_presence.mdx b/shared/chat-sdk/restful-api/_presence.mdx index 91747d283..6b4020966 100644 --- a/shared/chat-sdk/restful-api/_presence.mdx +++ b/shared/chat-sdk/restful-api/_presence.mdx @@ -5,7 +5,7 @@ The presence feature enables users to publicly display their online presence sta This page shows how to use the Chat RESTful APIs to implement presence in your project. Before calling the following methods, ensure that you meet the following: - You understand the call frequency limit of the Chat RESTful APIs as described in Limitations. -- You have activated the presence feature in [Agora Console](http://console.agora.io/). +- You have activated the presence feature in . ## Common parameters diff --git a/shared/chat-sdk/restful-api/_push-notification-management.mdx b/shared/chat-sdk/restful-api/_push-notification-management.mdx index 4e325c814..a62d74b2d 100644 --- a/shared/chat-sdk/restful-api/_push-notification-management.mdx +++ b/shared/chat-sdk/restful-api/_push-notification-management.mdx @@ -44,7 +44,7 @@ For each App Key, the total call frequency limit of this method is 1 per second. ### HTTP request -```http +```html POST https://{host}/{org_name}/{app_name}/push/sync/{target} ``` diff --git a/shared/chat-sdk/restful-api/_user-attributes-management.mdx b/shared/chat-sdk/restful-api/_user-attributes-management.mdx index 60da9d176..3108792b8 100644 --- a/shared/chat-sdk/restful-api/_user-attributes-management.mdx +++ b/shared/chat-sdk/restful-api/_user-attributes-management.mdx @@ -41,7 +41,7 @@ The following table lists common request and response parameters of the Chat RES ## Setting user attributes -User attributes are composed of multiple key-value pairs of attribute names and attribute values, and each attribute name has one corresponding attribute value. +Sets user attributes for a user. User attributes are composed of multiple key-value pairs of attribute names and attribute values, and each attribute name has one corresponding attribute value. For each App Key, the call frequency limit of this method is 100 per second. @@ -278,7 +278,7 @@ curl -X POST -H 'Authorization: Bearer {YourAppToken}' -H 'Content-Type: applic ## Retrieving the total size of user attributes in the app -This method retrieves the total size of user attributes under the app. +Retrieves the total size of user attributes under the app. For each App Key, the call frequency limit of this method is 100 per second. @@ -332,7 +332,7 @@ curl -X GET -H 'Authorization: Bearer {YourAppToken}''http://XXXX/XXXX/XXXX/meta ## Deleting user attributes -This method deletes all the user attributes of the specified user. +Deletes all the user attributes of the specified user. For each App Key, the call frequency limit of this method is 100 per second. diff --git a/shared/chat-sdk/restful-api/chat-group-management/_create-delete-retrieve-groups.mdx b/shared/chat-sdk/restful-api/chat-group-management/_create-delete-retrieve-groups.mdx index 247ef2590..1775907ba 100644 --- a/shared/chat-sdk/restful-api/chat-group-management/_create-delete-retrieve-groups.mdx +++ b/shared/chat-sdk/restful-api/chat-group-management/_create-delete-retrieve-groups.mdx @@ -47,7 +47,7 @@ Creates a new chat group and sets the group information. The group information i ### HTTP request -``` shell +```html POST https://{host}/{org_name}/{app_name}/chatgroups ``` @@ -70,13 +70,13 @@ For the descriptions of other path parameters, see [Common Parameters](#param). | `groupname` | String | The group name. It cannot exceed 128 characters. | No | | `description` | String | The group description. It cannot exceed 512 characters. | No | | `public` | Boolean | Whether the group is a public group. Public groups can be searched and chat users can apply to join a public group. Private groups cannot be searched, and chat users can join a private group only if the group owner or admin invites the user to join the group.
      • `true`: Yes
      • `false`: No
      | Yes | -| `scale` | String | The group scale. The parameter value depends on the setting of `maxusers`.
      • (Default) `normal`: Normal group that has a maximum of 3000 members.
      • `large`: Large group that has more than 3000 members. You must set this parameter when you create a large group. Large groups do not support offline push.
      | No | +| `scale` | String | The group scale. The parameter value depends on the setting of `maxusers`.
      • (Default) `normal`: Normal group that has a maximum of 3000 members.
      • `large`: Large group that has more than 3000 members. You must set this parameter when you create a large group. Large groups do not support offline push. To create a large group, contact [support@agoro.io](mailto:support@agoro.io).
      | No | | `maxusers` | String | The maximum number of chat group members (including the group owner). The default value is `200` for a normal group and `1000` for a large group. The upper limit varies with your price plans. For details, see [Pricing Plan Details](../../reference/pricing-plan-details#group). | No | -| `allowinvites` | Boolean | Whether a regular group member is allowed to invite other users to join the chat group.
      • `true`: Yes.
      • `false`: No. Only the group owner or admin can invite other users to join the chat group.
      | No | +| `allowinvites` | Boolean | Whether a regular group member is allowed to invite other users to join the chat group.
      • `true`: Yes.
      • `false`: (Default) No. Only the group owner or admin can invite other users to join the chat group.
      | No | | `membersonly` | Boolean | Whether the user requesting to join the public group requires approval from the group owner or admin:
      • `true`: Yes.
      • `false`: (Default) No.
      | No | | `invite_need_confirm` | Boolean | Whether the invitee needs to confirm the received group invitation before joining the group:
      • `true`: Yes.
      • `false`: No. The invitee automatically joins the chat group after receiving the group invitation.
      | No| | `owner` | String | The chat group owner. | Yes | -| `members` | Array | The array of user IDs of regular chat group members and admins, excluding the group owner. The number of user IDs in the array cannot exceed the value of `maxusers`. | No | +| `members` | Array | The array of user IDs of chat group members, excluding the group owner. The number of user IDs in the array cannot exceed the value of `maxusers`. | No | | `custom` | String | The extension information of the chat group. The extension information cannot exceed 1024 characters. | No | ### HTTP response @@ -135,7 +135,7 @@ Once a chat group is banned, the chat group members in the group can no longer s ### HTTP request -``` shell +```html POST https://{host}/{org_name}/{app_name}/chatgroups/{group_id}/disable ``` @@ -200,7 +200,7 @@ After unbanning a chat group, all chat group members regain permission to send a ### HTTP request -``` shell +```html POST https://{host}/{org_name}/{app_name}/chatgroups/{group_id}/enable ``` @@ -261,7 +261,7 @@ Checks whether a user has joined a group. ### HTTP request -```http +```html GET https://{host}/{org_name}/{app_name}/chatgroups/{group_id}/user/{username}/is_joined ``` @@ -521,7 +521,7 @@ Deletes the specified chat group. Once a chat group is deleted, all the threads ### HTTP request ```shell -DELETE https://{host}//{org_name}/{app_name}/chatgroups/{group_id} +DELETE https://{host}/{org_name}/{app_name}/chatgroups/{group_id} ``` #### Path parameter diff --git a/shared/chat-sdk/restful-api/chat-group-management/_manage-group-announcement-files.mdx b/shared/chat-sdk/restful-api/chat-group-management/_manage-group-announcement-files.mdx index bbc08bfb8..b06b25dd2 100644 --- a/shared/chat-sdk/restful-api/chat-group-management/_manage-group-announcement-files.mdx +++ b/shared/chat-sdk/restful-api/chat-group-management/_manage-group-announcement-files.mdx @@ -304,7 +304,6 @@ For the descriptions of other path parameters, see [Common Parameters](#param). | `Content-Type` | String | The parameter type. Set it as `application/json`. | Yes | | `Accept` | String | The parameter type. Set it as `application/json`. | Yes | | `Authorization` | String | The authentication token of the user or administrator, in the format of `Bearer ${token}`, where `Bearer` is a fixed character, followed by an English space, and then the obtained token value. | Yes | -| `restrict-access` | Boolean | Whether to restrict the access to the chat group shared file to chat group members only. | Yes | ### HTTP response @@ -330,7 +329,7 @@ If the returned HTTP status code is not 200, the request fails. You can refer to #### Request example ```shell -curl -X POST 'http://XXXX/XXXX/XXXX/chatgroups/66021836783617/share_files' -H 'Accept: application/json' -H 'Authorization: Bearer ' -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' -H 'restrict-access: true' -F file=@/Users/test/image/IMG_3.JPG +curl -X POST 'http://XXXX/XXXX/XXXX/chatgroups/66021836783617/share_files' -H 'Accept: application/json' -H 'Authorization: Bearer ' -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' -F file=@/Users/test/image/IMG_3.JPG ``` #### Response example @@ -389,18 +388,7 @@ For other parameters and detailed descriptions, see [Common parameters](#param). #### Response body -If the returned HTTP status code is 200, the request succeeds, and the `data` field in the response body contains the following parameters. - -| Parameter | Type | Description | -| :-------- | :----- | :----------------------------------------------------------- | -| `file_url` | String | The URL to the chat group shared file on the Chat server. | -| `group_id` | String | The group ID. | -| `file_name` | String | The name of the chat group shared file. | -| `created` | Long | The Unix timestamp for upload time of the chat group shared file. | -| `file_id` | String | The ID of the chat group shared file. This field is required if you want to download or remove a chat group shared file. | -| `file_size` | Number | The size of the group's shared file, in bytes. | - -For other fields and descriptions, see [Common parameter](#param). +If the returned HTTP status code is 200, the request succeeds and the response body contains only the content of the uploaded file. For example, the content of the uploaded file is `Hello world`, the response body only contains `Hello world`. If the returned HTTP status code is not 200, the request fails. You can refer to [Status code ](#code) for possible causes. @@ -414,26 +402,7 @@ curl -X GET -H 'Accept: application/json' -H 'Authorization: Bearer
    • `true`: Yes.
    • `false`: No.
    | -| `expire` | Long | The Unix timestamp when the mute state expires, in milliseconds. | +| `data.mute`| Boolean | Whether all the chat group members are muted.
    • `true`: Yes.
    • `false`: No.
    | For other fields and descriptions, see [Common parameter](#param). diff --git a/shared/chat-sdk/restful-api/chatroom-management/_manage-chatroom-attributes.mdx b/shared/chat-sdk/restful-api/chatroom-management/_manage-chatroom-attributes.mdx index b3dc8adb8..ef3088c99 100644 --- a/shared/chat-sdk/restful-api/chatroom-management/_manage-chatroom-attributes.mdx +++ b/shared/chat-sdk/restful-api/chatroom-management/_manage-chatroom-attributes.mdx @@ -75,7 +75,7 @@ For the parameters and detailed descriptions, see [Common parameters](#param). | Parameter | Type | Required | Description | | :------------ | :----- | :------- | :------ | -| `metaData` | JSON | Yes | The custom attributes that are stored as a collection of key-value pairs in the format `Map`. Keys in a map are unique attribute names that map to the corresponding attribute values. Note the following limitations:
    • Each chat room can have a maximum of 100 custom attributes.
    • The size of all custom attributes in an app can be a maximum of 10 GB.
    • Each map can contain a maximum of 10 key-value pairs.
    • Each key can contain a maximum of 128 characters.
    • Each value can contain a maximum of 4,086 characters.
    The following character sets are supported:
    • 26 lowercase English letters (a-z)
    • 26 uppercase English letters (A-Z)
    • 10 numbers (0-9)
    • "_", "-", "."
    | +| `metaData` | JSON | Yes | The custom attributes that are stored as a collection of key-value pairs in the format `Map`. Keys in a map are unique attribute names that map to the corresponding attribute values. Note the following limitations:
    • Each chat room can have a maximum of 100 custom attributes.
    • The size of all custom attributes in an app can be a maximum of 10 GB.
    • Each map can contain a maximum of 10 key-value pairs.
    • Each key can contain a maximum of 128 characters.
    • Each value can contain a maximum of 4,096 characters.
    The following character sets are supported:
    • 26 lowercase English letters (a-z)
    • 26 uppercase English letters (A-Z)
    • 10 numbers (0-9)
    • "_", "-", "."
    | | `autoDelete` | String | No | Whether to automatically delete the custom attributes set by a chat room member when this member leaves the chat room:
  4. (Default) DELETE: Delete the custom attributes.
  5. NO_DELETE: Do not delete the custom attributes.
  6. | ### HTTP response @@ -273,7 +273,7 @@ For the parameters and detailed descriptions, see [Common parameters](#param). | Parameter | Type | Required | Description | | :------------ | :----- | :------- | :------ | -| `metaData` | JSON | Yes | The custom attributes that are stored as a collection of key-value pairs in the format `Map`. Keys in a map are unique attribute names that map to the corresponding attribute values. Note the following limitations:
    • Each chat room can have a maximum of 100 custom attributes.
    • The size of all custom attributes in an app can be a maximum of 10 GB.
    • Each map can contain a maximum of 10 key-value pairs.
    • Each key can contain a maximum of 128 characters.
    • Each value can contain a maximum of 4,086 characters.
    The following character sets are supported:
    • 26 lowercase English letters (a-z)
    • 26 uppercase English letters (A-Z)
    • 10 numbers (0-9)
    • "_", "-", "."
    | +| `metaData` | JSON | Yes | The custom attributes that are stored as a collection of key-value pairs in the format `Map`. Keys in a map are unique attribute names that map to the corresponding attribute values. Note the following limitations:
    • Each chat room can have a maximum of 100 custom attributes.
    • The size of all custom attributes in an app can be a maximum of 10 GB.
    • Each map can contain a maximum of 10 key-value pairs.
    • Each key can contain a maximum of 128 characters.
    • Each value can contain a maximum of 4,096 characters.
    The following character sets are supported:
    • 26 lowercase English letters (a-z)
    • 26 uppercase English letters (A-Z)
    • 10 numbers (0-9)
    • "_", "-", "."
    | | `autoDelete` | String | No | Whether to automatically delete the custom attributes set by a chat room member when this member leaves the chat room:
  7. (Default) DELETE: Delete the custom attributes.
  8. NO_DELETE: Do not delete the custom attributes.
  9. | ### HTTP response diff --git a/shared/chat-sdk/restful-api/chatroom-management/_manage-chatrooms.mdx b/shared/chat-sdk/restful-api/chatroom-management/_manage-chatrooms.mdx index 03c465365..bbaf8824a 100644 --- a/shared/chat-sdk/restful-api/chatroom-management/_manage-chatrooms.mdx +++ b/shared/chat-sdk/restful-api/chatroom-management/_manage-chatrooms.mdx @@ -66,7 +66,7 @@ The request body is a JSON object, which contains the following fields: | :------------ | :--------- | :--------------------------------------- | :------- | | `name` | String | The chat room name which can contain a maximum of 128 characters. | Yes | | `description` | String | The chat room description which can contain a maximum of 512 characters. | Yes | -| `maxusers` | Int | The maximum number of members (including the chat room owner) that can join a chat room. The default value is `10,000`. | No | +| `maxusers` | Int | The maximum number of members (including the chat room owner) that can join a chat room. The value range is [1,10,000], with `1000` as the default. To increase the upper limit, contact [support@agora.io](mailto:support@agora.io). | No | | `owner` | String | The username of the chat room creator. | Yes | | `members` | JSONArray | The array of user IDs of regular chat room members and administrators, excluding the chat room owner. If you specify this parameter, remember to pass in at least one user ID. The number of user IDs in the array cannot exceed the value of `maxusers`.| No | @@ -286,7 +286,6 @@ Retrieves the detailed information of one or more specified chat rooms. ### HTTP request ```json -# Replace with the app token generated in your server. GET https://{host}/{org_name}/{app_name}/chatrooms/{chatroom_id} ``` diff --git a/shared/chat-sdk/restful-api/shared/common-parameters.mdx b/shared/chat-sdk/restful-api/shared/common-parameters.mdx index a47d4b27a..98295ab25 100644 --- a/shared/chat-sdk/restful-api/shared/common-parameters.mdx +++ b/shared/chat-sdk/restful-api/shared/common-parameters.mdx @@ -19,7 +19,7 @@ The following table lists common request and response parameters of the Chat RES | `applicationName` | String | The unique identifier assigned to each app by the Chat service . This is the same as `app_name`. | | `uri` | String | The request URI. | | `path` | String | The request path, which is part of the request URL. You can safely ignore this parameter. | -| `entities ` | JSON | The response entity. | +| `entities ` | JSON Array | The response entity. | | `entities.uuid` | String | The user's UUID. A unique internal identifier generated by the Chat service for the user in this request. This is used for generating the user token. | | `entities.type` | String | The type of the object. You can safely ignore this parameter. | | `entities.created` | Number | The Unix timestamp (ms) when the user is registered. | diff --git a/shared/chat-sdk/restful-api/thread-management/_create-delete-retrieve-threads.mdx b/shared/chat-sdk/restful-api/thread-management/_create-delete-retrieve-threads.mdx index b202f0407..e5c01fb29 100644 --- a/shared/chat-sdk/restful-api/thread-management/_create-delete-retrieve-threads.mdx +++ b/shared/chat-sdk/restful-api/thread-management/_create-delete-retrieve-threads.mdx @@ -435,7 +435,9 @@ For the descriptions of the request headers, see [Authorization](#auth). #### Response body -If the returned HTTP status code is `200`, the request succeeds, and the entity field in the response body contains the following parameters: +If the returned HTTP status code is `200`, the request succeeds, and the entity field in the response body contains the following parameters. + +For the last page of data, the response still contains `cursor` and the number of retrieved threads is smaller than the value of `limit` in the request. If there is no more thread data returned in the response, you have retrieved data of all threads in this group. | Parameter | Type | Description | | :------- |:-------------|:-------------| diff --git a/shared/chat-sdk/restful-api/thread-management/_manage-thread-members.mdx b/shared/chat-sdk/restful-api/thread-management/_manage-thread-members.mdx index d416df5b5..cdcaf1845 100644 --- a/shared/chat-sdk/restful-api/thread-management/_manage-thread-members.mdx +++ b/shared/chat-sdk/restful-api/thread-management/_manage-thread-members.mdx @@ -188,7 +188,7 @@ For each App Key, the call frequency limit of this method is 100 per second. ### HTTP request ```html -DELETE https://{host}/{org_name}/{app_name}/threads/{thread_id}/users +DELETE https://{host}/{org_name}/{app_name}/thread/{thread_id}/users ``` #### Path parameter diff --git a/shared/cloud-gateway/develop/_cloud-proxy.mdx b/shared/cloud-gateway/develop/_cloud-proxy.mdx index 1eb9466b3..fd6b12e21 100644 --- a/shared/cloud-gateway/develop/_cloud-proxy.mdx +++ b/shared/cloud-gateway/develop/_cloud-proxy.mdx @@ -42,12 +42,12 @@ The Agora Cloud Proxy service has a tiered capacity structure based on your mont | Tier 1
    (Default) | 200 or fewer | 500 | | Tier 2 | From 201 to 1,000 | 1,000 | | Tier 3 | From 1,001 to 2,000 | 2,000 | -| Tier 4 | 2,001 or more | [Contact Sales](https://www.agora.io/en/talk-to-us/) or request support through the [Agora Console](https://console.agora.io/) | +| Tier 4 | 2,001 or more | [Contact Sales](https://www.agora.io/en/talk-to-us/) or request support through the | **Notes** - Typically, about 5% to 10% of the audio and video traffic from your end users may require the cloud proxy service. -- Tier 1 is the default capacity of the Agora Cloud Proxy service. You can activate the cloud proxy service for Tier 1 directly from the [Agora Console](https://console.agora.io/). If you want to use the cloud proxy service for a Tier higher than Tier 1, you must contact [Agora Customer Support](mailto:support@agora.io) to help with activation or deactivation. +- Tier 1 is the default capacity of the Agora Cloud Proxy service. You can activate the cloud proxy service for Tier 1 directly from the . If you want to use the cloud proxy service for a Tier higher than Tier 1, you must contact [Agora Customer Support](mailto:support@agora.io) to help with activation or deactivation. - Agora offers a 10% overage allowance for all tiers of PCU at no additional charge. Agora notifies you when your PCU exceeds the 10% overage allowance. Upon receiving this notice, contact [Agora Customer Support](mailto:support@agora.io) to upgrade to the next tier to ensure you have sufficient capacity in the future. Before your upgrade, Agora does try to provide the required capacity beyond the 10% overage allowance; however, the quality of service for an overage of greater than 10% is not guaranteed. ### Usage-based fee @@ -82,16 +82,16 @@ Force UDP and Force TCP cloud proxy minutes count toward the [10,000 free-of-cha Once the Force UDP or Force TCP cloud proxy service is activated per your request, you are billed according to the minimum monthly base fee or based upon minutes of usage, whichever is greater. Billing occurs at the end of each calendar month. -You can deactivate the Cloud Proxy Force UDP or Force TCP modes at any time on the [Agora Console](https://console.agora.io/). The deactivation takes effect immediately, and you are billed either the minimum monthly base fee or the usage-based fee for that month, whichever is greater. +You can deactivate the Cloud Proxy Force UDP or Force TCP modes at any time on the . The deactivation takes effect immediately, and you are billed either the minimum monthly base fee or the usage-based fee for that month, whichever is greater. ## Developer Guide ### Enabling Cloud Proxy Force UDP and Force TCP Modes from the Agora Console -Follow these steps to enable the Force UDP or Force TCP cloud proxy service on the [Agora Console](https://console.agora.io/): +Follow these steps to enable the Force UDP or Force TCP cloud proxy service on the : -1. Log in to the [Agora Console](https://console.agora.io/), and click the **Project Management** icon on the left navigation panel. +1. Log in to the , and click the **Project Management** icon on the left navigation panel. 2. On the **Project Management** page, click **Config** for the project for which you want to enable the cloud proxy service. ![1646734465912](https://web-cdn.agora.io/docs-files/1646734465912) @@ -111,9 +111,9 @@ Follow these steps to enable the Force UDP or Force TCP cloud proxy service on t **Notes** - If your estimated PCU during a month exceeds 200, and you want to use either Force UDP or Force TCP mode, -you cannot activate the service on the [Agora Console](https://console.agora.io/) directly. Contact [Agora Customer Support](mailto:support@agora.io) for help with the activation. -- If your estimated PCU during a month is 200 or fewer, you can activate either the Force UDP or Force TCP mode directly on the [Agora Console](https://console.agora.io/), and it takes effect within 24 hours. -- After activation, you can deactivate the service at any time on the [Agora Console](https://console.agora.io/), and the deactivation takes effect immediately. +you cannot activate the service on the directly. Contact [Agora Customer Support](mailto:support@agora.io) for help with the activation. +- If your estimated PCU during a month is 200 or fewer, you can activate either the Force UDP or Force TCP mode directly on the , and it takes effect within 24 hours. +- After activation, you can deactivate the service at any time on the , and the deactivation takes effect immediately. ### Enabling Cloud Proxy Force UDP and Force TCP Modes from the SDKs @@ -123,7 +123,7 @@ you cannot activate the service on the [Agora Console](https://console.agora.io/ Before enabling the cloud proxy modes in the SDK, ensure you meet the following prerequisites: 1. Your end users have configured their firewalls to trust the Cloud Proxy Allowed IP List (see Allowed IP List on Agora Console). -2. You have enabled Cloud Proxy Force UDP and Force TCP modes, either through the [Agora Console](https://console.agora.io/) (for capacity Tier 1) or by contacting [Agora Customer Support](mailto:support@agora.io). The status on the Console indicates **Enabled**. +2. You have enabled Cloud Proxy Force UDP and Force TCP modes, either through the (for capacity Tier 1) or by contacting [Agora Customer Support](mailto:support@agora.io). The status on the Console indicates **Enabled**. 3. You have integrated the SDK and prepared the development environment. #### Implementation diff --git a/shared/cloud-gateway/develop/_media-stream-encryption.mdx b/shared/cloud-gateway/develop/_media-stream-encryption.mdx index 2d4e59954..c5f13d4fc 100644 --- a/shared/cloud-gateway/develop/_media-stream-encryption.mdx +++ b/shared/cloud-gateway/develop/_media-stream-encryption.mdx @@ -20,7 +20,7 @@ To generate and set the `key` and `salt` parameters, refer to the following step 1. Refer to the following command to randomly generate a 32-byte key in the string format through OpenSSL on your server. - ``` shell + ```shell # Randomly generate a 32-byte key in the string format, and pass the string key in the encryptionKey parameter of enableEncryption. openssl rand -hex 32 dba643c8ba6b6dc738df43d9fd624293b4b12d87a60f518253bd10ba98c48453 @@ -32,7 +32,7 @@ To generate and set the `key` and `salt` parameters, refer to the following step 1. Refer to the following command to randomly generate a Base64-encoded, 32-byte salt through OpenSSL on the server. You can also refer to the [C++ sample code](https://github.com/AgoraIO/Tools/blob/master/DynamicKey/AgoraDynamicKey/cpp/sample/RtcChannelEncryptionSaltSample.cpp) provided by Agora on GitHub to randomly generate a salt in the byte array format and convert it to Base64 on the server. - ``` shell + ```shell # Randomly generate a 32-byte salt in the Base64 format. Convert the salt from Base64 to uint8_t, and pass the uint8_t salt in the encryptionKdfSalt parameter of enableEncryption. openssl rand -base64 32 X5w9T+50kzxVOnkJKiY/lUk82/bES2kATOt3vBuGEDw= diff --git a/shared/cloud-gateway/reference/_pricing.mdx b/shared/cloud-gateway/reference/_pricing.mdx index 19faae109..757d307d4 100644 --- a/shared/cloud-gateway/reference/_pricing.mdx +++ b/shared/cloud-gateway/reference/_pricing.mdx @@ -1,3 +1,3 @@ import * as data from '@site/data/variables'; -[Contact Sales](https://www.agora.io/en/talk-to-us/) or request support through [Agora Console](https://console.agora.io/). +[Contact Sales](https://www.agora.io/en/talk-to-us/) or request support through . diff --git a/shared/cloud-gateway/reference/release-notes/cpp.mdx b/shared/cloud-gateway/reference/release-notes/cpp.mdx index 0a6d783c8..4d0285103 100644 --- a/shared/cloud-gateway/reference/release-notes/cpp.mdx +++ b/shared/cloud-gateway/reference/release-notes/cpp.mdx @@ -2,18 +2,31 @@ import * as data from '@site/data/variables'; -## v3.8.200.20 - -This version was released on July 22, 2022, with the following features: - -- Sending and receiving video and audio data in various formats. -- Independent audio/video and sending/receiving processes. -- Starting a single process with multiple SDK instances. -- Specifying remote streams by user ID. -- Using a user ID in string format to join a channel. -- Audio mixing. -- Video mixing -- Media encryption. -- Cloud proxy. -- Network geofencing. - \ No newline at end of file +## Version 4.2.30 + +v4.2.30 was released on February 6, 2024. + +### New features + +- Multipath network transmission + + This release adds the multipath network transmission feature, which allows simultaneous transmission of audio and video streams over multiple network paths. For example, when you insert two IoT SIM cards in the device, the SDK can transmit audio and video streams through the network of both SIM cards, thereby improving the stability and reliability of audio and video transmission. Contact support@agora.io to enable and use this feature. + +- Hardware encoding acceleration + + This release supports utilizing NVIDIA GPU hardware acceleration for video encoding, which reduces the power consumption of CPU and improves hardware encoding efficiency and performance. Contact support@agora.io to enable and use this feature. + +### Improvements + +- Audio sending timestamp + + To set the timestamp at which you send the audio data, this release adds the `presentation_ms` parameter in the `sendAudioPcmData` method. In scenarios such as video conferences and remote collaboration, this parameter allows you to achieve audio and video synchronization. + +### Bug fixes + +This release fixed the following issues: + +- App memory growth caused by receiving H.264 video raw data. +- Garbled data stream when using both string-type user IDs and data stream functions simultaneously. + +
    diff --git a/shared/cloud-gateway/reference/release-notes/java.mdx b/shared/cloud-gateway/reference/release-notes/java.mdx index 4a2883b3e..b685c536d 100644 --- a/shared/cloud-gateway/reference/release-notes/java.mdx +++ b/shared/cloud-gateway/reference/release-notes/java.mdx @@ -2,17 +2,4 @@ import * as data from '@site/data/variables'; -## v3.7.200.21 - -This version was released on July 22, 2022 with the following features: - -- Sending and receiving video and audio data in various formats. -- Independent video/audio and sending/receiving process. -- Starting a single process with multiple SDK instances. -- Using string user ID to join a channel. -- Audio mixing. -- Media encryption. -- Cloud proxy. -- Network geofencing. - - \ No newline at end of file +
    diff --git a/shared/common/_channel-management-api.mdx b/shared/common/_channel-management-api.mdx index 99b0dd4e8..6e3df83fa 100644 --- a/shared/common/_channel-management-api.mdx +++ b/shared/common/_channel-management-api.mdx @@ -102,7 +102,7 @@ Pass in the following parameters in the request body: | Parameter | Data type | Required/Optional | Description | | :------ | :----- |:----- | :---------------------- | -| `appid` | String | Required |The App ID of the project. You can get it through one of the following methods:
  10. Copy from the [Agora Console](https://console.agora.io)
  11. Call the [Get all projects](/interactive-live-streaming/reference/agora-console-rest-api#get-all-projects) API, and read the value of the `vendor_key` field in the response body.
  12. | +| `appid` | String | Required |The App ID of the project. You can get it through one of the following methods:
  13. Copy from the
  14. Call the [Get all projects](/interactive-live-streaming/reference/agora-console-rest-api#get-all-projects) API, and read the value of the `vendor_key` field in the response body.
  15. | | `cname` | String | Optional |The channel name. | | `uid` | Number | Optional |The user ID. Do not set it as `0`. | | `ip` | String | Optional |The IP address of the user. Do not set it as `0`. | @@ -169,7 +169,7 @@ Pass the following query parameters in the request URL: | Parameter | Type | Required/Optional |Description | | :------ | :---------------- |:------| :------------------------------ | -| `appid` | String | Required | The App ID of the project. You can get it through one of the following methods:
  16. Copy from the [Agora Console](https://console.agora.io).
  17. Call the [Get all projects](/interactive-live-streaming/reference/agora-console-rest-api#get-all-projects) API, and read the value of the `vendor_key` field in the response body.
  18. | +| `appid` | String | Required | The App ID of the project. You can get it through one of the following methods:
  19. Copy from the .
  20. Call the [Get all projects](/interactive-live-streaming/reference/agora-console-rest-api#get-all-projects) API, and read the value of the `vendor_key` field in the response body.
  21. | #### Response parameters @@ -229,7 +229,7 @@ Pass in the following parameters in the request body: | Parameter | Type |Required/Optional | Description | | :---------------- | :-----|:----- | :--------------------------------------- | -| `appid` | String | Required | The App ID of the project. You can get it through one of the following methods:
  22. Copy from the [Agora Console](https://console.agora.io).
  23. Call the [Get all projects](/interactive-live-streaming/reference/agora-console-rest-api#get-all-projects) API, and read the value of the `vendor_key` field in the response body.
  24. | +| `appid` | String | Required | The App ID of the project. You can get it through one of the following methods:
  25. Copy from the .
  26. Call the [Get all projects](/interactive-live-streaming/reference/agora-console-rest-api#get-all-projects) API, and read the value of the `vendor_key` field in the response body.
  27. | | `id` | Number | Required |The ID of the rule that you want to update. | | `time` | Number | Required | The time duration (in minutes) to ban the user. The value range is [1,1440].

    **Note**

  28. If the set value is between `0` and `1`, Agora automatically sets the value to `1`.
  29. If the set value is greater than `1440`, Agora automatically sets the value to `1440`.
  30. If the set value is `0`, the banning rule does not take effect. The server sets all users that conform to the rule offline, and users can log in again to rejoin the channel.
  31. Use either `time` or `time_in_seconds`. If you set both parameters, the `time_in_seconds` parameter takes effect; if you set neither of these parameters, the Agora server automatically sets the banning time duration to 60 minutes, that is, 3600 seconds.
  32. | | `time_in_seconds` | Number | Required | The time duration (in seconds) to ban the user. The value range is [10,86430].

    **Note**

  33. If the set value is between `0` and `10`, Agora automatically sets the value to `10`.
  34. If the set value is greater than `86430`, Agora automatically sets the value to `86430`.
  35. If the set value is `0`, the banning rule does not take effect. The server sets all users that conform to the rule offline, and users can log in again to rejoin the channel.
  36. Use either `time` or `time_in_seconds`. If you set both parameters, the `time_in_seconds` parameter takes effect; if you set neither of these parameters, the Agora server automatically sets the banning time duration to 60 minutes, that is, 3600 seconds.
  37. | @@ -288,7 +288,7 @@ The following parameters are required in the request body: | Parameter | Type | Required/Optional | Description | | :------ | :----- |:----- | :---------------------- | -| `appid` | String | Required | The App ID of the project. You can get it through one of the following methods:
  38. Copy from the [Agora Console](https://console.agora.io).
  39. Call the [Get all projects](/interactive-live-streaming/reference/agora-console-rest-api#get-all-projects) API, and read the value of the `vendor_key` field in the response body.
  40. | +| `appid` | String | Required | The App ID of the project. You can get it through one of the following methods:
  41. Copy from the .
  42. Call the [Get all projects](/interactive-live-streaming/reference/agora-console-rest-api#get-all-projects) API, and read the value of the `vendor_key` field in the response body.
  43. | | `id` | Number | Required | The ID of the rule that you want to delete. | #### Request example @@ -349,7 +349,7 @@ Pass the following path parameters in the request URL: | Parameter | Type | Required/Optional | Description | | :------ | :----- |:----- | :---------------------- | -| `appid` | String | Required | The App ID of the project. You can get it through one of the following methods:
  44. Copy from the [Agora Console](https://console.agora.io).
  45. Call the [Get all projects](/interactive-live-streaming/reference/agora-console-rest-api#get-all-projects) API, and read the value of the `vendor_key` field in the response body.
  46. | +| `appid` | String | Required | The App ID of the project. You can get it through one of the following methods:
  47. Copy from the .
  48. Call the [Get all projects](/interactive-live-streaming/reference/agora-console-rest-api#get-all-projects) API, and read the value of the `vendor_key` field in the response body.
  49. | | `uid` | Number | Required | The user ID.

    **Note**: This parameter does not support string user accounts. Ensure that you use the integer user ID.

    | | `channelName` | String | Required | The channel name. | @@ -420,7 +420,7 @@ Pass the following path parameters in the request URL: | Parameter | Type | Required/Optional | Description | | :------ | :----- |:----- | :---------------------- | -| `appid` | String | Required |The App ID of the project. You can get it through one of the following methods:
  50. Copy from the [Agora Console](https://console.agora.io).
  51. Call the [Get all projects](/interactive-live-streaming/reference/agora-console-rest-api#get-all-projects) API, and read the value of the `vendor_key` field in the response body.
  52. | +| `appid` | String | Required |The App ID of the project. You can get it through one of the following methods:
  53. Copy from the .
  54. Call the [Get all projects](/interactive-live-streaming/reference/agora-console-rest-api#get-all-projects) API, and read the value of the `vendor_key` field in the response body.
  55. | | `channelName` | String | Required | The channel name. | #### Response parameters @@ -497,7 +497,7 @@ Pass the following path parameters in the request URL: | Parameter | Type | Required/Optional | Description | | :------ | :----- |:----- | :---------------------- | -| `appid` | String | Required | The App ID of the project. You can get it through one of the following methods:
  56. Copy from the [Agora Console](https://console.agora.io).
  57. Call the [Get all projects](/interactive-live-streaming/reference/agora-console-rest-api#get-all-projects) API, and read the value of the `vendor_key` field in the response body.
  58. | +| `appid` | String | Required | The App ID of the project. You can get it through one of the following methods:
  59. Copy from the .
  60. Call the [Get all projects](/interactive-live-streaming/reference/agora-console-rest-api#get-all-projects) API, and read the value of the `vendor_key` field in the response body.
  61. | **Query parameters** diff --git a/shared/common/_core-concepts.mdx b/shared/common/_core-concepts.mdx index b2fbddfea..8944a76b9 100644 --- a/shared/common/_core-concepts.mdx +++ b/shared/common/_core-concepts.mdx @@ -3,7 +3,7 @@ Before you start writing your code, familiarize yourself with the core concepts ## -[](https://console.agora.io/) is the main dashboard where you manage your projects and services. provides an intuitive interface for developers to query and manage their account. After [registering an account](https://console.agora.io/en/signup), you use the console to perform the following tasks: + is the main dashboard where you manage your projects and services. provides an intuitive interface for developers to query and manage their account. After registering an Agora Account, you use the console to perform the following tasks: - Manage the account - Create and configure projects and services @@ -32,7 +32,7 @@ may be installed on a device at a time. ## App ID -The App ID is a random string generated within [](https://console.agora.io/) when you create a new project. You can create multiple projects in your account; each project has a different App ID. This App ID enables your app users to communicate securely with each other. When you initialize in your app, you pass the App ID as an argument. The App ID is also used to create the authentication tokens that ensure secure communication in a channel. You [retrieve your App ID](https://console.agora.io/projects) using . +The App ID is a random string generated within when you create a new project. You can create multiple projects in your account; each project has a different App ID. This App ID enables your app users to communicate securely with each other. When you initialize in your app, you pass the App ID as an argument. The App ID is also used to create the authentication tokens that ensure secure communication in a channel. You retrieve your App ID using . uses this App ID to identify each app, provide billing and other statistical data services. diff --git a/shared/common/_restful-authentication.mdx b/shared/common/_restful-authentication.mdx index 3a9a0ccd7..979a32e14 100644 --- a/shared/common/_restful-authentication.mdx +++ b/shared/common/_restful-authentication.mdx @@ -27,7 +27,7 @@ Implement authentication on the server; otherwise, you may encounter the risk of To generate a set of Customer ID and Customer Secret, do the following: -1. In [Agora Console](https://console.agora.io/), click the account name in the top right corner, and click **RESTful API** from the drop-down list to enter the **RESTful API** page. +1. In , click the account name in the top right corner, and click **RESTful API** from the drop-down list to enter the **RESTful API** page. ![1637661003647](https://web-cdn.agora.io/docs-files/1637661003647) diff --git a/shared/common/manage-agora-account/_agora-console-restapi.mdx b/shared/common/manage-agora-account/_agora-console-restapi.mdx index d45bb0e52..d825218c5 100644 --- a/shared/common/manage-agora-account/_agora-console-restapi.mdx +++ b/shared/common/manage-agora-account/_agora-console-restapi.mdx @@ -51,7 +51,7 @@ Pass in the following parameters in the request body: **Request body** -``` json +```json { "name": "project1", "enable_sign_key": true @@ -75,7 +75,7 @@ If the status code is `201`, the request succeeds, and the response body include The following is a response example for a successful request: -``` json +```json { "project": { "id": "xxxx", @@ -132,7 +132,7 @@ If the status code is `201`, the request succeeds, and the response body include The following is a response example for a successful request: -``` json +```json { "projects": [ { @@ -180,7 +180,7 @@ If the status code is `201`, the request succeeds, and the response body include The following is a response example for a successful request: -``` json +```json { "projects": [ { @@ -230,7 +230,7 @@ Pass in the following parameters in the request body: **Request body** -``` json +```json { "id": "xxxx", "status": 0 @@ -253,7 +253,7 @@ If the status code is `201`, the request succeeds, and the response body include The following is a response example for a successful request: -``` json +```json { "project": { "id": "xxxx", @@ -292,7 +292,7 @@ Pass in the following parameters in the request body: **Request body** -``` json +```json { "id": "xxxx", "recording_server": "10.12.1.5:8080" @@ -315,7 +315,7 @@ If the status code is `201`, the request succeeds, and the response body include The following is a response example for a successful request: -``` json +```json { "project": { "id": "xxxx", @@ -354,7 +354,7 @@ The following parameters are required in the request body: **Request body** -``` json +```json { "id": "xxxx", "enable": true @@ -377,7 +377,7 @@ If the status code is `201`, the request succeeds, and the response body include The following is a response example for a successful request: -``` json +```json { "project": { "id": "xxxx", @@ -415,7 +415,7 @@ Pass in the following parameter in the request body: **Request body** -``` json +```json { "id": "xxxx" } @@ -437,7 +437,7 @@ If the status code is `201`, the request succeeds, and the response body include The following is a response example for a successful request: -``` json +```json { "project": { "id": "xxxx", @@ -497,7 +497,7 @@ If the status code is `200`, the request succeeds, and the response body include The following is a response example for a successful request: -``` json +```json { "meta": { "durationAudioAll": { diff --git a/shared/common/manage-agora-account/_check-usage.mdx b/shared/common/manage-agora-account/_check-usage.mdx index 0192b70f1..24521439c 100644 --- a/shared/common/manage-agora-account/_check-usage.mdx +++ b/shared/common/manage-agora-account/_check-usage.mdx @@ -15,7 +15,7 @@ This page introduces how to check your usage of Agora products in Agora Console, Follow these steps to check your usage: -1. Enter the [**Usage**](https://console.agora.io/duration) page in Console. +1. Enter the Usage page in Console. 2. (Optional) Add filters for the usage data, as follows: diff --git a/shared/common/manage-agora-account/_console-overview.mdx b/shared/common/manage-agora-account/_console-overview.mdx index 85721039b..816d86af6 100644 --- a/shared/common/manage-agora-account/_console-overview.mdx +++ b/shared/common/manage-agora-account/_console-overview.mdx @@ -4,7 +4,7 @@ import * as data from '@site/data/variables'; ### Agora Console Overview -[Agora Console](https://console.agora.io/) is the unified portal for you to configure, purchase, and manage Agora products and services. This page introduces the main features of Agora Console. + is the unified portal for you to configure, purchase, and manage Agora products and services. This page introduces the main features of Agora Console. #### Overview dashboard diff --git a/shared/common/manage-agora-account/_delete-account.mdx b/shared/common/manage-agora-account/_delete-account.mdx index a0482eca7..a15aa2974 100644 --- a/shared/common/manage-agora-account/_delete-account.mdx +++ b/shared/common/manage-agora-account/_delete-account.mdx @@ -10,15 +10,15 @@ Before deleting your Agora account, ensure that the following requirements are m - You are the creator of the account to be deleted. -- The account to be deleted is an Agora account created in [Agora Console](https://console.agora.io/), not a reseller account created in [Agora Reseller Console](https://reseller.agora.io/). +- The account to be deleted is an Agora account created in , not a reseller account created in [Agora Reseller Console](https://reseller.agora.io/). -- There are no active projects under your account. If there are any, go to the [Project Management](https://console.agora.io/projects) page to disable all active projects. +- There are no active projects under your account. If there are any, go to the Project Management page to disable all active projects. - Your account balance is zero. - If your account balance is positive, go to the [Withdraw](https://console.agora.io/finance/withdraw) page to request a withdrawal. -- If your account balance is negative, go to [Billing Center](https://console.agora.io/finance) to add money to your account. +- If your account balance is negative, go to Billing to add money to your account. - Your bill for last month has been issued. @@ -32,7 +32,7 @@ Before deleting your Agora account, ensure that the following requirements are m - Extensions Marketplace packages. -- There are no members under your account. If there are any, go to the [Member Management](https://console.agora.io/settings/member) page to delete all members. +- There are no members under your account. If there are any, go to the Teams and Members page to delete all members. #### Procedure diff --git a/shared/common/manage-agora-account/_get-appid-token.mdx b/shared/common/manage-agora-account/_get-appid-token.mdx index 72e6d06ee..88d6cb785 100644 --- a/shared/common/manage-agora-account/_get-appid-token.mdx +++ b/shared/common/manage-agora-account/_get-appid-token.mdx @@ -6,7 +6,7 @@ To join a real-time engagement session powered by Agora, you need to provide an #### Create an Agora account -Sign up [here](https://sso.agora.io/en/signup/) to create an Agora account. After sign-up, you can log in [here](https://console.agora.io/). +[Sign-up](https://sso.agora.io/en/signup/) to create an Agora account. After sign-up, log in to . #### Create an Agora project @@ -14,7 +14,7 @@ Once you finish the sign-up process, you can create an Agora project in Agora Co To create an Agora project, do the following: -1. In , open the [Project Management](https://console.agora.io/projects) page. +1. In , open the Project Management page. 2. Click **Create**. @@ -26,7 +26,7 @@ To create an Agora project, do the following: Agora automatically assigns each project an App ID as a unique identifier. -To copy this App ID, find your project on the [Project Management](https://console.agora.io/projects) page in Agora Console, and click the copy icon in the App ID column. +To copy this App ID, find your project on the Project Management page in Agora Console, and click the copy icon in the App ID column. #### Get the App Certificate @@ -34,7 +34,7 @@ When generating a token on your app server, you need to fill parameters such as To get an App Certificate, do the following: -1. On the [Project Management](https://console.agora.io/projects) page, click **Config** for the project you want to use. +1. On the Project Management page, click **Config** for the project you want to use. ![1641971710869](https://web-cdn.agora.io/docs-files/1641971710869) 2. Click the copy icon under **Primary Certificate**. @@ -46,7 +46,7 @@ The Agora RESTful API uses a Customer ID and Customer Secret for basic HTTP auth To generate a set of Customer ID and Customer Secret, do the following: -1. In [Agora Console](https://console.agora.io/), click the account name in the top right corner, and click **RESTful API** from the drop-down list to enter the **RESTful API** page. +1. In , click the account name in the top right corner, and click **RESTful API** from the drop-down list to enter the **RESTful API** page. ![1637661003647](https://web-cdn.agora.io/docs-files/1637661003647) 2. Click **Add a secret**, and click **OK**. A set of Customer ID and Customer Secret is generated. @@ -60,8 +60,7 @@ To ensure communication security, best practice is to use tokens to authenticate To generate a temporary token: -1. In [ > Project Management](https://console.agora.io/projects), select your project and click -**Configure**. +1. On the Project Management page, select your project and click **Configure**. 1. In your browser, navigate to the [ token builder](https://agora-token-generator-demo.vercel.app/). diff --git a/shared/common/manage-agora-account/_manage-member.mdx b/shared/common/manage-agora-account/_manage-member.mdx index 9a09e57f7..f07327af9 100644 --- a/shared/common/manage-agora-account/_manage-member.mdx +++ b/shared/common/manage-agora-account/_manage-member.mdx @@ -10,7 +10,7 @@ This page shows how to manage members and roles in Agora Console. Follow these steps to add a member to your account: -1. Log in to [Agora Console](https://console.agora.io/), click your account name in the top-right corner, and click **Setting** in the dropdown menu. +1. Log in to , click your account name in the top-right corner, and click **Setting** in the dropdown menu. ![1638242972239](https://web-cdn.agora.io/docs-files/1638242972239) 2. In the left navigation panel, click **Member management**. @@ -21,17 +21,19 @@ Follow these steps to add a member to your account: #### Manage members -On the [Member Management](https://console.agora.io/settings/member) page, you can do the following: +On the Member Management page, you can do the following: - Click the edit icon to reset the role and permissions of a member. - Click the delete icon to remove a member from your account. + Only the main account can delete a member account. Member accounts with the Admin role cannot delete a member account. + #### Manage roles On the **Role Management** page, you can view the permissions of predefined roles or add a custom role with certain permissions. -To enter the **Role Management** page, click **Role management** in the left navigation panel of the [Settings](https://console.agora.io/settings) page. +To enter the **Role Management** page, click **Role management** in the left navigation panel of the Settings page. #### View predefined roles diff --git a/shared/common/manage-agora-account/_manage-profile.mdx b/shared/common/manage-agora-account/_manage-profile.mdx index 74ce9e9bb..3158e2ad4 100644 --- a/shared/common/manage-agora-account/_manage-profile.mdx +++ b/shared/common/manage-agora-account/_manage-profile.mdx @@ -8,7 +8,7 @@ This page shows how to view and edit the profile of your Agora account. To enter the **Profile** page, follow these steps: -1. Log in to [Agora Console](https://console.agora.io/). +1. Log in to . 2. Click your account name in the top-right corner, and select your account name from the dropdown menu. diff --git a/shared/common/manage-agora-account/_manage-projects.mdx b/shared/common/manage-agora-account/_manage-projects.mdx index de639de01..7910e7896 100644 --- a/shared/common/manage-agora-account/_manage-projects.mdx +++ b/shared/common/manage-agora-account/_manage-projects.mdx @@ -13,7 +13,7 @@ This page shows how to create a project, view project information, and manage ap To create an Agora project, do the following: -1. Enter the [Project Management](https://console.agora.io/projects) page. +1. Enter the Project Management page. 2. Click **Create**. diff --git a/shared/common/manage-agora-account/_online-payment.mdx b/shared/common/manage-agora-account/_online-payment.mdx index be5edcf89..0c0feef21 100644 --- a/shared/common/manage-agora-account/_online-payment.mdx +++ b/shared/common/manage-agora-account/_online-payment.mdx @@ -9,7 +9,7 @@ If your account has multiple members, only those with the role of **Admin**, **F #### Check account balance -After logging in to [Agora Console](https://console.agora.io/), you can see your account balance in the **Billing Center** panel on the **Overview** page. +After logging in to , you can see your account balance in the **Billing Center** panel on the **Overview** page. #### Add money to account @@ -19,7 +19,7 @@ You can add money to your account either with a credit card or via bank transfer To add money to your account with a credit card, follow these steps: -1. In [Billing Center](https://console.agora.io/finance), click **Credit Card** in the left navigation panel. +1. In Billing, click **Credit Card**. 2. (Optional) If you have not added a credit card to your account, follow these steps: @@ -37,13 +37,13 @@ To add money to your account with a credit card, follow these steps: To add money to your account via bank transfer, follow these steps: -1. In [Billing Center](https://console.agora.io/finance), click **Bank Transfer** in the left navigation panel. +1. In Billing, click **Bank Transfer**. 2. Follow the on-screen instructions to complete. #### View transactions -In [Billing Center](https://console.agora.io/finance), click **Transactions** in the left navigation panel. You can see all the transactions in your account. +In Billing, click **Transactions**. You can see all the transactions in your account. You can also click **Export CSV** in the top-right corner to export the record of transactions as a CSV file. diff --git a/shared/common/manage-agora-account/_sign-in-and-sign-up.mdx b/shared/common/manage-agora-account/_sign-in-and-sign-up.mdx index 9f4aea5ab..decfecc44 100644 --- a/shared/common/manage-agora-account/_sign-in-and-sign-up.mdx +++ b/shared/common/manage-agora-account/_sign-in-and-sign-up.mdx @@ -38,7 +38,7 @@ This page shows how to sign up for an Agora account and log in to Agora Console. #### Sign up with a third-party account -1. On the [Console login page](https://console.agora.io), choose a third-party account you want to use. +1. On the login page, choose a third-party account you want to use. 2. Follow the on-screen instructions to complete verification. @@ -50,6 +50,6 @@ This page shows how to sign up for an Agora account and log in to Agora Console. Once you sign up successfully, your account is automatically logged in. Follow the on-screen instructions to create your first project and test out real-time communications. -For later visits, log in to [Agora Console](https://console.agora.io) with your phone number, email address, or linked third-party account. +For later visits, log in to with your phone number, email address, or linked third-party account. diff --git a/shared/common/manage-agora-account/_ticket.mdx b/shared/common/manage-agora-account/_ticket.mdx index 0e52d3e1a..2702120e3 100644 --- a/shared/common/manage-agora-account/_ticket.mdx +++ b/shared/common/manage-agora-account/_ticket.mdx @@ -6,7 +6,7 @@ To ask Agora support a question: Follow these steps: -1. Log in to [Agora Console](https://console.agora.io/). +1. Log in to . 2. Click **Support** in the top navigation menu. diff --git a/shared/common/no-uikit-wo-wrappers.mdx b/shared/common/no-uikit-wo-wrappers.mdx index 6ad08b87c..44be3389d 100644 --- a/shared/common/no-uikit-wo-wrappers.mdx +++ b/shared/common/no-uikit-wo-wrappers.mdx @@ -1,3 +1,3 @@ import * as data from '@site/data/variables'; -**Currently, there is no UI Samples for this platform.** \ No newline at end of file +**Currently, there is no UI Kit for .** \ No newline at end of file diff --git a/shared/common/no-uikit.mdx b/shared/common/no-uikit.mdx index d2cfc74f2..c09b182f3 100644 --- a/shared/common/no-uikit.mdx +++ b/shared/common/no-uikit.mdx @@ -1,8 +1,5 @@ import * as data from '@site/data/variables'; - -**Currently, there is no for this platform.** - - -**Currently, there is no for this platform.** + +**Currently, there is no for .** diff --git a/shared/common/prerequities-get-started.mdx b/shared/common/prerequities-get-started.mdx index bf681ea76..efbd15db4 100644 --- a/shared/common/prerequities-get-started.mdx +++ b/shared/common/prerequities-get-started.mdx @@ -18,7 +18,9 @@ - A [supported browser](../reference/supported-platforms#browsers). + - Physical media input devices, such as a camera and a microphone. + - A JavaScript package manager such as [npm](https://www.npmjs.com/package/npm). diff --git a/shared/common/prerequities.mdx b/shared/common/prerequities.mdx index ccd8c287f..084c13f6c 100644 --- a/shared/common/prerequities.mdx +++ b/shared/common/prerequities.mdx @@ -1,20 +1,18 @@ - -- [Android Studio](https://developer.android.com/studio) 4.1 or higher. -- Android SDK API Level 24 or higher. -- A mobile device that runs Android 4.1 or higher. +To test the code used in this page you need to have: + + +* Implemented either of the following: + - [ quickstart](../get-started/get-started-uikit) + - [SDK quickstart](../get-started/get-started-sdk) - -- Xcode 12.0 or higher. -- A device running iOS 9.0 or higher. + +* Setup the [ reference app](../get-started/get-started-sdk#prerequisites) for . - -- Xcode 12.0 or higher. -- A device running macOs 10.11 or higher. -- An Apple developer account + +* Same setup as the [ quickstart prerequisites](../get-started/get-started-sdk#prerequisites) for . - -- A device running Windows 7 or higher. -- Microsoft Visual Studio 2017 or higher with [Desktop development with C++](https://devblogs.microsoft.com/cppblog/windows-desktop-development-with-c-in-visual-studio/) support. + +* Implemented the [SDK quickstart](../get-started/get-started-sdk) - [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/) or higher with C++ and desktop development support. @@ -29,6 +27,7 @@ | [Windows](https://docs.unrealengine.com/4.27/en-US/Basics/InstallingUnrealEngine/RecommendedSpecifications/) | 32-bit Windows only supports Unreal Engine 4 and below. You need to uncomment the Windows 32-bit related code in the file `AgoraPluginLibrary.Build.cs`. | + - A [supported browser](../reference/supported-platforms#browsers). diff --git a/shared/common/project-setup/android.mdx b/shared/common/project-setup/android.mdx index 7d39d7c09..4bf4e3803 100644 --- a/shared/common/project-setup/android.mdx +++ b/shared/common/project-setup/android.mdx @@ -1,36 +1,31 @@ - -1. **Clone the reference app to `` on your development environment**: +1. **Clone the [ reference app](https://github.com/AgoraIO/video-sdk-samples-android) repository**. Navigate to your `` folder and run the following command: - - ```bash - git clone https://github.com/AgoraIO/video-sdk-samples-android.git - ``` - + + ```bash + git clone https://github.com/AgoraIO/video-sdk-samples-android.git + ``` + 1. **Open the reference app in Android Studio**: From the **File** menu select **Open...** then navigate to `/video-sdk-samples-android/android-reference-app` and click **OK**. Android Studio loads the project and Gradle sync downloads the dependencies. - +1. **Clone the [ reference app](https://github.com/AgoraIO/signaling-sdk-samples-android) repository**. -1. **Clone the repository**: - - To clone the repository to your local machine, open Terminal and navigate to the directory where you want to clone the repository. Then, use the following command: + Navigate to your `` folder and run the following command: ```bash git clone https://github.com/AgoraIO/signaling-sdk-samples-android ``` -1. **Install the dependencies**: - - Launch Android Studio. From the **File** menu, select **Open...** and navigate to the `signaling-reference-app` folder. Start Gradle sync to automatically install the project dependencies. +1. **Open the reference app in Android Studio**: - By default is installed automatically. However, you can also [Install manually](../reference/downloads#through-the-agora-website). + From the **File** menu select **Open...** then navigate to `/signaling-sdk-samples-android/signaling-reference-app` and click **OK**. Android Studio loads the project and Gradle sync downloads the dependencies. diff --git a/shared/common/project-setup/electron.mdx b/shared/common/project-setup/electron.mdx index 92aef3df2..c6bd811f2 100644 --- a/shared/common/project-setup/electron.mdx +++ b/shared/common/project-setup/electron.mdx @@ -1,22 +1,22 @@ -1. Take the following steps to setup a new Electron project: + 1. Take the following steps to setup a new Electron project: - 1. Open a terminal window and navigate to the directory where you want to create the project. + 1. Open a terminal window and navigate to the directory where you want to create the project. - 2. Execute the following command in the terminal: + 2. Execute the following command in the terminal: - ``` bash - git clone https://github.com/electron/electron-quick-start - ``` - This command clones the Electron quick-start project that you use to implement . + ```bash + git clone https://github.com/electron/electron-quick-start + ``` + This command clones the Electron quick-start project that you use to implement . -2. Install the + 2. Install the - Open a terminal window in your project folder and execute the following command to download and install the . + Open a terminal window in your project folder and execute the following command to download and install the . - ``` bash - npm i agora-electron-sdk - ``` - Make sure the path to your project folder does not contain any spaces. This might cause error during the installation. + ```bash + npm i agora-electron-sdk + ``` + Make sure the path to your project folder does not contain any spaces. This might cause error during the installation. diff --git a/shared/common/project-setup/flutter.mdx b/shared/common/project-setup/flutter.mdx index a5492c6a9..0cd874004 100644 --- a/shared/common/project-setup/flutter.mdx +++ b/shared/common/project-setup/flutter.mdx @@ -9,19 +9,14 @@ ``` Flutter helps diagnose any issues with your development environment. Make sure that your system passes all the checks. - -1. **Clone the reference app to `` on your development environment**: + +1. **Clone the [ reference app](https://github.com/AgoraIO/video-sdk-samples-flutter) repository** Navigate to your `` folder and run the following command: - - ```bash - git clone https://github.com/AgoraIO/video-sdk-samples-flutter.git - ``` - - - - + ```bash + git clone https://github.com/AgoraIO/video-sdk-samples-flutter.git + ``` 1. **Open the reference app in your IDE** diff --git a/shared/common/project-setup/index.mdx b/shared/common/project-setup/index.mdx index 7e5d00c67..a5a3a5c5c 100644 --- a/shared/common/project-setup/index.mdx +++ b/shared/common/project-setup/index.mdx @@ -1,17 +1,25 @@ import Android from './android.mdx'; import Ios from './ios.mdx'; -import Macos from './macos.mdx'; +import MacOS from './macos.mdx'; +import Web from './web.mdx'; import ReactNative from './react-native.mdx'; +import ReactJS from './react-js.mdx'; +import Electron from './electron.mdx'; +import Flutter from './flutter.mdx'; import Unity from './unity.mdx'; -import Web from './web.mdx'; import Windows from './windows.mdx'; +import Unreal from './unreal.mdx'; +import Linux from './linux-cpp.mdx'; - supplies a single GitHub reference repository. This runnable and testable repo contains all the code used in this documentation. To create a development environment: - + + + + + + - - - + + diff --git a/shared/common/project-setup/ios.mdx b/shared/common/project-setup/ios.mdx index af8cebccc..f5f4f899c 100644 --- a/shared/common/project-setup/ios.mdx +++ b/shared/common/project-setup/ios.mdx @@ -1,13 +1,12 @@ - + -1. **Clone the reference app to `` on your development environment**: +1. **Clone the [ reference app](https://github.com/AgoraIO/video-sdk-samples-ios) to +`` on your development machine**: - Navigate to your `` folder and run the following command: - - ```bash - git clone https://github.com/AgoraIO/video-sdk-samples-ios.git - ``` + ```bash + git clone https://github.com/AgoraIO/video-sdk-samples-ios.git + ``` 1. **Open the sample project in Xcode**: @@ -15,29 +14,22 @@ xcodeproj` and click **Open**. Xcode loads the project. 1. **Connect a physical or virtual device to your development environment**. - +1. **Clone the [ reference app](https://github.com/AgoraIO/signaling-sdk-samples-ios) to +`` on your development machine**: -1. **Clone the repository** - - To clone the repository to your local machine, open Terminal and navigate to the directory where you want to clone the repository. Then, use the following command: - - ```sh + ```bash git clone https://github.com/AgoraIO/signaling-sdk-samples-ios.git ``` -1. **Open the project** +1. **Open the sample project in Xcode**: - Navigate to `Example-App`, and open `Example-App.xcodeproj`. - - All dependencies are automatically installed through the Swift Package Manager. + Select **File** > **Open...** then navigate to `/signaling-sdk-samples-ios/Example-App/Example-App.xcodeproj` and click **Open**. Xcode loads the project. 1. **Update Signing** In the Xcode project, open the target, add your development team, and make the bundle identifier unique under **Signing & Capabilities**. - - diff --git a/shared/common/project-setup/linux-cpp.mdx b/shared/common/project-setup/linux-cpp.mdx new file mode 100644 index 000000000..c805827aa --- /dev/null +++ b/shared/common/project-setup/linux-cpp.mdx @@ -0,0 +1,22 @@ + + + + +1. **Clone the [Signaling SDK reference app](https://github.com/AgoraIO/signaling-sdk-samples-linux) repository** + + Navigate to your `` folder and run the following command: + + ```bash + git clone https://github.com/saudsami/signaling-sdk-samples-linux.git + ``` + +1. **Integrate the Signaling SDK for Linux** + + 1. [Download](../../sdks) the Agora Signaling SDK and unzip the contents. + + 1. Copy the file `libagora_rtm_sdk.so` to the `/signaling-sdk-samples-linux/lib` folder. + + 1. Copy the `include` folder to the `/signaling-sdk-samples-linux` folder. + + + \ No newline at end of file diff --git a/shared/common/project-setup/macos.mdx b/shared/common/project-setup/macos.mdx index bd41a0c09..11b218136 100644 --- a/shared/common/project-setup/macos.mdx +++ b/shared/common/project-setup/macos.mdx @@ -1,41 +1,33 @@ + 1. Clone the [ sample project](https://github.com/AgoraIO/video-sdk-samples-macos) to `` on + your + development machine: -1. **Clone the reference app to `` on your development environment**: + ```bash + git clone https://github.com/AgoraIO/video-sdk-samples-macos.git + ``` - Navigate to your `` folder and run the following command: - - ```bash - git clone https://github.com/AgoraIO/video-sdk-samples-macos.git - ``` - + 1. Open the sample project in Xcode. -1. Open the sample project in Xcode. - - Select **File** > **Open...** then navigate to `/video-sdk-samples-macos/Docs-Examples.xcodeproj` and click **Open**. Xcode loads the project. - + Select **File** > **Open...** then navigate to `/video-sdk-samples-macos/Docs-Examples.xcodeproj` and click **Open**. Xcode loads the project. +1. **Clone the [ reference app](https://github.com/AgoraIO/signaling-sdk-samples-ios) to +`` on your development machine**: -1. **Clone the repository** - - To clone the repository to your local machine, open Terminal and navigate to the directory where you want to clone the repository. Then, use the following command: - - ```sh + ```bash git clone https://github.com/AgoraIO/signaling-sdk-samples-ios.git ``` -1. **Open the project** +1. **Open the sample project in Xcode**: - Navigate to `Example-App`, and open `Example-App.xcodeproj`. - - All dependencies are automatically installed through the Swift Package Manager. + Select **File** > **Open...** then navigate to `/signaling-sdk-samples-ios/Example-App/Example-App.xcodeproj` and click **Open**. Xcode loads the project. 1. **Update Signing** In the Xcode project, open the target, add your development team, and make the bundle identifier unique under **Signing & Capabilities**. - \ No newline at end of file diff --git a/shared/common/project-setup/new-project/android.mdx b/shared/common/project-setup/new-project/android.mdx new file mode 100644 index 000000000..1e35fe8da --- /dev/null +++ b/shared/common/project-setup/new-project/android.mdx @@ -0,0 +1,70 @@ + + +1. In Android Studio, create a new **Phone and Tablet** [Android project](https://developer.android.com/studio/projects/create-project) with an **Empty Activity**. + + After creating the project, Android Studio automatically starts gradle sync. Ensure that the sync succeeds before you continue. + +2. Integrate the into your Android project: + + These steps are for package install, if you prefer to manually install, follow the [installation instructions](../reference/downloads#manual-installation). + + 1. In `/Gradle Scripts/build.gradle (Module: .app)`, add the following line under `dependencies`: + + ```groovy + implementation 'io.agora.rtc::' + ``` + + + 2. Replace `` and `` with appropriate values for the latest release. For example, `io.agora.rtc:voice-sdk:4.2.6`. + + You can get the latest package manager link from the [Download SDKs](../../sdks?platform=android) page. + + + 2. Replace `` and `` with appropriate values for the latest release. For example, `io.agora.rtc:voice-sdk:4.2.6`. + + You can get the latest package manager link from the [Download SDKs](../../sdks?platform=android) page. + + +3. Add permissions for network and device access. + + In `/app/Manifests/AndroidManifest.xml`, add the following permissions after ``: + + + ``` java + + + + + + + + + + + + + ``` + + + + ``` java + + + + + + + + + + + + ``` + + +4. To prevent obfuscating the code in , add the following line to `/Gradle Scripts/proguard-rules.pro`: + + ``` java + -keep class io.agora.**{*;} + ``` + \ No newline at end of file diff --git a/shared/common/project-setup/new-project/blueprint.mdx b/shared/common/project-setup/new-project/blueprint.mdx new file mode 100644 index 000000000..f8f1e6902 --- /dev/null +++ b/shared/common/project-setup/new-project/blueprint.mdx @@ -0,0 +1,17 @@ + + +### Set up a Blueprint project + +1. Launch Unreal Engine and [create a new project](https://docs.unrealengine.com/5.3/en-US/creating-a-new-project-in-unreal-engine). +1. Under **New Project Categories**, choose **Games** and select **Blank** for the template. +1. On the **Project Settings** page, input the following settings: + - Choose **Blueprint**. + - Select **Desktop/Console** or **Mobile/Tablet** project type according to your target platform. + - Set the project storage path and give the project a name. + - Click **Create Project** to create your project. +1. Integrate the SDK into your project. + 1. [Download](https://docs.agora.io/en/sdks) the latest version of Unreal Video SDK and unzip it. + 1. In your project root folder, create a `Plugins` folder. + 1. Copy the `AgoraPlugin` folder inside the `Unreal Video SDK` directory to `Plugins`. + + \ No newline at end of file diff --git a/shared/common/project-setup/new-project/electron.mdx b/shared/common/project-setup/new-project/electron.mdx new file mode 100644 index 000000000..16da793e5 --- /dev/null +++ b/shared/common/project-setup/new-project/electron.mdx @@ -0,0 +1,22 @@ + + + 1. Take the following steps to setup a new Electron project: + + 1. Open a terminal window and navigate to the directory where you want to create the project. + + 2. Execute the following command in the terminal: + + ``` bash + git clone https://github.com/electron/electron-quick-start + ``` + This command clones the Electron quick-start project that you use to implement . + + 2. Install the + + Open a terminal window in your project folder and execute the following command to download and install the . + + ``` bash + npm i agora-electron-sdk + ``` + Make sure the path to your project folder does not contain any spaces. This might cause error during the installation. + \ No newline at end of file diff --git a/shared/video-sdk/get-started/get-started-sdk/project-setup/flutter.mdx b/shared/common/project-setup/new-project/flutter.mdx similarity index 100% rename from shared/video-sdk/get-started/get-started-sdk/project-setup/flutter.mdx rename to shared/common/project-setup/new-project/flutter.mdx diff --git a/shared/common/project-setup/new-project/index.mdx b/shared/common/project-setup/new-project/index.mdx new file mode 100644 index 000000000..abae563f5 --- /dev/null +++ b/shared/common/project-setup/new-project/index.mdx @@ -0,0 +1,24 @@ +import Android from './android.mdx'; +import Blueprint from './blueprint.mdx'; +import Ios from './ios.mdx'; +import MacOS from './macos.mdx'; +import Web from './web.mdx'; +import ReactNative from './react-native.mdx'; +import ReactJS from './react-js.mdx'; +import Electron from './electron.mdx'; +import Flutter from './flutter.mdx'; +import Unity from './unity.mdx'; +import Windows from './windows.mdx'; +import Unreal from './unreal.mdx'; + + + + + + + + + + + + diff --git a/shared/video-sdk/develop/spatial-audio/project-implementation/ios.mdx b/shared/common/project-setup/new-project/ios.mdx similarity index 81% rename from shared/video-sdk/develop/spatial-audio/project-implementation/ios.mdx rename to shared/common/project-setup/new-project/ios.mdx index f21f0f8b6..638588df0 100644 --- a/shared/video-sdk/develop/spatial-audio/project-implementation/ios.mdx +++ b/shared/common/project-setup/new-project/ios.mdx @@ -4,4 +4,4 @@ import Source from './swift.mdx'; - + \ No newline at end of file diff --git a/shared/video-sdk/develop/real-time-transcription/project-test/macos.mdx b/shared/common/project-setup/new-project/macos.mdx similarity index 100% rename from shared/video-sdk/develop/real-time-transcription/project-test/macos.mdx rename to shared/common/project-setup/new-project/macos.mdx diff --git a/shared/common/project-setup/new-project/react-js.mdx b/shared/common/project-setup/new-project/react-js.mdx new file mode 100644 index 000000000..5996d0ac7 --- /dev/null +++ b/shared/common/project-setup/new-project/react-js.mdx @@ -0,0 +1,26 @@ + + + +1. **Install NodeJS**: + + Ensure that [Node.js](https://nodejs.org/en) is installed on your development machine. + +1. **Initialize a new project** + + Open a terminal and run the following command to create and initialize a new project using `Vite`: + + ```bash + npx create-vite my-agora-project --template react-ts + ``` + This command uses `Vite` to scaffold a new React project with `TypeScript`. + +1. **Install the project dependencies**: + + Install Agora SDK and the extensions required for your project: + + ```bash + cd my-agora-project + npm install agora-rtc-sdk-ng agora-rtc-react agora-extension-ai-denoiser agora-extension-spatial-audio agora-extension-virtual-background --save + ``` + + \ No newline at end of file diff --git a/shared/common/project-setup/new-project/react-native.mdx b/shared/common/project-setup/new-project/react-native.mdx new file mode 100644 index 000000000..e45dcb562 --- /dev/null +++ b/shared/common/project-setup/new-project/react-native.mdx @@ -0,0 +1,61 @@ + + +1. **Setup a React Native environment for project** + + In the terminal, run the following command: + + ```bash + npx react-native init ProjectName --template react-native-template-typescript + ``` + + npx creates a new boilerplate project in the `ProjectName` folder. + + For Android projects, enable the project to use Android SDK. In the `android` folder of your project, set the `sdk.dir` in the `local.properties` file. For example: + + + ```bash + sdk.dir=C:\\PATH\\TO\\ANDROID\\SDK + ``` + +1. **Test the setup** + + Launch your Android or iOS simulator and run your project by executing the following command: + + 1. Run `npx react-native start` in the root of your project to start Metro. + 1. Open another terminal in the root of your project and run `npx react-native run-android` to start the Android app, or run `npx react-native run-ios` to start the iOS app. + + You see your new app running in your Android or iOS simulator. You can also run your project on a physical Android or iOS device. For detailed instructions, see [Running on device](https://reactnative.dev/docs/running-on-device). + +1. **Integrate and configure ** + + To integrate on React Native 0.60.0 or later: + 1. Navigate to the root folder of your project in the terminal and integrate with either: + - npm + + ```bash + npm i --save react-native-agora + ``` + + - yarn + + ```bash + // Install yarn. + npm install -g yarn + // Download the Agora React Native SDK using yarn. + yarn add react-native-agora + ``` + + Do not link native modules manually, React Native 0.60.0 and later support [Autolinking](https://github.com/react-native-community/cli/blob/main/docs/autolinking.md). + + 1. If your target platform is iOS, use CocoaPods to install : + + ```bash + npx pod-install + ``` + + 1. uses Swift in native modules, your project must support compiling Swift. To create `File.swift`: + + 1. In Xcode, open `ios/ProjectName.xcworkspace`. + 1. Click **File > New > File, select iOS > Swift File**, then click **Next > Create** . + + \ No newline at end of file diff --git a/shared/common/project-setup/new-project/swift.mdx b/shared/common/project-setup/new-project/swift.mdx new file mode 100644 index 000000000..4c4ddca33 --- /dev/null +++ b/shared/common/project-setup/new-project/swift.mdx @@ -0,0 +1,55 @@ + +1. [Create a new project](https://help.apple.com/xcode/mac/current/#/dev07db0e578) for this using the **App** template. Select the **Storyboard** Interface and **Swift** Language. + + If you have not already added team information, click **Add account…**, input your Apple ID, then click **Next**. + +1. [Enable automatic signing](https://help.apple.com/xcode/mac/current/#/dev23aab79b4) for your project. + + [Set the target devices](https://help.apple.com/xcode/mac/current/#/deve69552ee5) to deploy your iOS to an iPhone or iPad. + +1. Add project permissions for microphone and camera usage: + + 1. Open **Info** in the project navigation panel, then add the following properties to the [Information Property List](https://help.apple.com/xcode/mac/current/#/dev3f399a2a6): + + | Key | Type | Value | + |------------------------------|--------|------------------------| + | NSMicrophoneUsageDescription | String | Access the microphone. | + | NSCameraUsageDescription | String | Access the camera. | + + + 2. Add sandbox and runtime capabilities to your project: + + Open the target for your project in the project navigation properties, then add the following capabilities in **Signing & Capabilities**. + - **App Sandbox**: + - Incoming Connections (Server) + - Outgoing Connections (Client) + - Camera + - Audio Input + - **Hardened Runtime**: + - Camera + - Audio Input + + +1. Integrate into your project: + + These steps are for package install, if you prefer to use **CocoaPods** or manually install, follow the [installation instructions](../reference/downloads#manual-installation). + + 1. In Xcode, click **File** > **Add Packages**, then paste the following link in the search: + + ``` + https://github.com/AgoraIO/AgoraRtcEngine_macOS.git + ``` + + + ``` + https://github.com/AgoraIO/AgoraRtcEngine_iOS.git + ``` + + You see the available packages. Add the **** package and any other functionality that you want to integrate into your app. For example, _AgoraAINoiseSuppressionExtension_. Choose a version later than 4.0.0. + + 1. Click **Add Package**. In the new window, make sure to choose **** and click **Add Package**. + + You see **AgoraRtcKit** in **Package Dependencies** for your project. + + + diff --git a/shared/common/project-setup/new-project/unity.mdx b/shared/common/project-setup/new-project/unity.mdx new file mode 100644 index 000000000..ee489df11 --- /dev/null +++ b/shared/common/project-setup/new-project/unity.mdx @@ -0,0 +1,21 @@ + +1. **Create a Unity project**: + + 1. In Unity Hub, select **Projects**, then click **New Project**. + + 1. In **All templates**, select **3D**. Set the **Project name** and **Location**, then click **Create Project**. + + 1. In **Projects**, double-click the project you created. Your project opens in Unity. + + +1. **Integrate **: + 1. Go to [SDKs](/sdks), download the latest version of the Agora , and unzip the downloaded SDK to a local folder. + + 1. In Unity, click **Assets > Import Package > Custom Package**. + + 1. Navigate to the package and click *Open*. + + 1. In **Import Unity Package**, click **Import**. + + + \ No newline at end of file diff --git a/shared/common/project-setup/new-project/unreal.mdx b/shared/common/project-setup/new-project/unreal.mdx new file mode 100644 index 000000000..9795bd53e --- /dev/null +++ b/shared/common/project-setup/new-project/unreal.mdx @@ -0,0 +1,29 @@ + +1. **Create a new project** + + 1. In Unreal Editor, select the **Games** new project category. + + 1. From **Unreal Project Browser**, select a **Blank template** and choose the following settings from **Project Defaults**: + + 1. Select **C++**. + + 1. From the **Target Platform** dropdown, select **Desktop**. + + 1. Clear **Starter Content**. + + 1. In the **Project Name** field, input `AgoraImplementation` and click **Create**. + + Your project opens in Unreal Editor. + +1. **Integrate ** + + 1. Create a folder called `Plugins` in the root directory of your project folder. + + 1. Unzip the latest version of the [](/sdks) to `/Plugins`. + + 1. In Solution Explorer, right-click your project, then click **Properties**. The **AgoraImplementation Property Pages** window opens. Go to the **VC++ Directories** menu and add the following string in the **External Include Directories** field, then click **OK**: + + ``` + $(SolutionDir)Plugins\AgoraPlugin\Source\AgoraPlugin\Public; + ``` + \ No newline at end of file diff --git a/shared/common/project-setup/new-project/web.mdx b/shared/common/project-setup/new-project/web.mdx new file mode 100644 index 000000000..3345643c9 --- /dev/null +++ b/shared/common/project-setup/new-project/web.mdx @@ -0,0 +1,37 @@ + + +1. Create a new project using [Vite](https://vitejs.dev/) + + 1. Open a terminal window and navigate to the directory where you want to create the project. + + 2. Execute the following command in the terminal: + + ``` bash + npm create vite@latest agora_project --template vanilla + ``` + + When prompted to select a framework, choose `Vanilla` and when prompted to select a variant, choose `JavaScript`. + A directory named *agora\_project* is created which contains the project files. We will update the following files in the directory: + + - *index.html*: The visual interface with the user. + + - *main.js*: The programmable interface used to implement the logic. + +2. Install the dependencies: + + In the terminal, navigate to the *agora\_project* directory, and execute the following command. + + ``` bash + npm install + ``` + +3. Install the : + + Execute the following command in the terminal to download and install the . + + ``` bash + npm i agora-rtc-sdk-ng + ``` + + These steps are for package install, if you prefer to manually install, follow the [installation instructions](../reference/downloads#manual-installation). + \ No newline at end of file diff --git a/shared/common/project-setup/new-project/windows.mdx b/shared/common/project-setup/new-project/windows.mdx new file mode 100644 index 000000000..45ca55387 --- /dev/null +++ b/shared/common/project-setup/new-project/windows.mdx @@ -0,0 +1,59 @@ + + + +1. **Create an MFC dialog-based application** + + 1. From the main menu, choose **File** > **New** > **Project**. + + 1. Enter **MFC** into the search box and then choose **MFC App** from the result list. + + 1. In **Project Name**, input `AgoraImplementation` and press **Create** to open the **MFC Application Wizard**. + + 1. In **MFC Application**, under **Application type** , select **Dialog based**, then click **Finish**. Your project opens in Visual Studio. + + +1. **Integrate ** + + To integrate the into your project. + + 1. Unzip the latest version of [](/sdks) in a local directory. + + 1. Copy the `sdk` directory of the downloaded SDK package to the root of your project, ``. + + 1. Create a new directory `/Debug`. + + 1. Copy the files from `` to `/Debug`. + + +1. **Configure your project properties** + + Right-click the project name In **Solution Explorer**, then click **Properties** to configure the following project properties, and click **OK**. + + 1. In **AgoraImplementation Property pages**, select **Win32** from the **Platform** dropdown list. + + If your targeted platform is `x64`, then select `x64` from the **Platform** dropdown list. + + 1. Go to the **C/C++** > **General** > **Additional Include Directories** menu, click **Edit**, and input the following string in the pop-up window: + + ``` + ;$(SolutionDir)sdk\high_level_api\include;$(ProjectDir) + ``` + + 1. Go to the **Linker** > **General** > **Additional Library Directories** menu, click **Edit**, and input the following string in the pop-up window: + + ``` + ;$(SolutionDir)sdk\x86; + ``` + The sample code uses `x86` platform. If you are using `x64`, then add the following string to the **Additional Library Directories** field: + + ``` + ;$(SolutionDir)sdk\x86_64 + ``` + + 1. Go to the **C/C++** > **Preprocessor** > **Preprocessor Definitions** menu, click **Edit**, and input the following path in the pop-up window: + + ``` + ;_CRT_SECURE_NO_WARNINGS + ``` + + \ No newline at end of file diff --git a/shared/common/project-setup/react-js.mdx b/shared/common/project-setup/react-js.mdx index ae897132d..3d2939281 100644 --- a/shared/common/project-setup/react-js.mdx +++ b/shared/common/project-setup/react-js.mdx @@ -1,23 +1,22 @@ -1. **Clone the reference app to `` on your development environment**: + +1. **Clone the [ reference app](https://github.com/AgoraIO/video-sdk-samples-reactjs) to your + development environment**: - Navigate to your `` folder and run the following command: - ```bash git clone https://github.com/AgoraIO/video-sdk-samples-reactjs ``` - - - 1. **Install the dependencies**: In Terminal, navigate to `video-sdk-samples-reactjs`, and execute the following command. - ``` bash + ```bash npm install ``` is installed automatically. However, you can also [Install manually](../reference/downloads#through-the-agora-website). - + + + \ No newline at end of file diff --git a/shared/common/project-setup/react-native.mdx b/shared/common/project-setup/react-native.mdx index 0a243f29b..e45dcb562 100644 --- a/shared/common/project-setup/react-native.mdx +++ b/shared/common/project-setup/react-native.mdx @@ -1,33 +1,61 @@ +1. **Setup a React Native environment for project** -1. **Clone the reference app to `` on your development environment**: + In the terminal, run the following command: - Navigate to your `` folder and run the following command: - - ```bash - git clone https://github.com/AgoraIO/video-sdk-samples-reactnative.git - ``` - - + ```bash + npx react-native init ProjectName --template react-native-template-typescript + ``` - + npx creates a new boilerplate project in the `ProjectName` folder. + For Android projects, enable the project to use Android SDK. In the `android` folder of your project, set the `sdk.dir` in the `local.properties` file. For example: -1. **Install project dependencies** - 1. Open this project in an IDE and launch a terminal. + ```bash + sdk.dir=C:\\PATH\\TO\\ANDROID\\SDK + ``` - 1. Install the required dependencies: +1. **Test the setup** - ```bash - yarn install - ``` + Launch your Android or iOS simulator and run your project by executing the following command: - 1. For iOS, run the following command: + 1. Run `npx react-native start` in the root of your project to start Metro. + 1. Open another terminal in the root of your project and run `npx react-native run-android` to start the Android app, or run `npx react-native run-ios` to start the iOS app. - ```bash - npx pod-install - ``` + You see your new app running in your Android or iOS simulator. You can also run your project on a physical Android or iOS device. For detailed instructions, see [Running on device](https://reactnative.dev/docs/running-on-device). - +1. **Integrate and configure ** + + To integrate on React Native 0.60.0 or later: + 1. Navigate to the root folder of your project in the terminal and integrate with either: + - npm + + ```bash + npm i --save react-native-agora + ``` + + - yarn + + ```bash + // Install yarn. + npm install -g yarn + // Download the Agora React Native SDK using yarn. + yarn add react-native-agora + ``` + + Do not link native modules manually, React Native 0.60.0 and later support [Autolinking](https://github.com/react-native-community/cli/blob/main/docs/autolinking.md). + + 1. If your target platform is iOS, use CocoaPods to install : + + ```bash + npx pod-install + ``` + + 1. uses Swift in native modules, your project must support compiling Swift. To create `File.swift`: + + 1. In Xcode, open `ios/ProjectName.xcworkspace`. + 1. Click **File > New > File, select iOS > Swift File**, then click **Next > Create** . + + \ No newline at end of file diff --git a/shared/common/project-setup/unity.mdx b/shared/common/project-setup/unity.mdx index a89147a4c..77033dea7 100644 --- a/shared/common/project-setup/unity.mdx +++ b/shared/common/project-setup/unity.mdx @@ -1,31 +1,56 @@ -1. **Clone the reference app to `` on your development environment**: + + +1. **Clone the repository** + + To clone the repository to your local machine, open Terminal and navigate to the directory where you want to clone the repository. Then, use the following command: - Navigate to your `` folder and run the following command: - ```bash - git https://github.com/AgoraIO/video-sdk-samples-unity.git + git clone https://github.com/AgoraIO/video-sdk-samples-unity.git ``` - - - - 1. **Open the project** 1. In Unity Hub, Open `video-sdk-samples-unity`, Unity Editor opens the project. - Unity Editor warns of compile errors. Don't worry, you fix them when you import Video SDK for Unity. + If Unity Editor warns you of compile errors. don't worry; you fix them when you import Video SDK for Unity. - 1. Unzip [the latest version of the Agora Video SDK](https://docs.agora.io/en/sdks?platform=unity) to a local folder. + 1. Download and extract the latest version of the [Agora Video SDK](https://docs.agora.io/en/sdks?platform=unity) to a local folder. - 1. In **Unity**, click **Assets** > **Import Package** > **Custom Package**. + 1. In Unity, click **Assets** > **Import Package** > **Custom Package**. 1. Navigate to the Video SDK package and click **Open**. 1. In **Import Unity Package**, click **Import**. Unity recompiles the Video SDK samples for Unity and the warnings disappear. + + +1. **Clone the [ reference app](https://github.com/AgoraIO/signaling-sdk-samples-unity) repository**. - + Navigate to your `` folder and run the following command: + + ```bash + git clone https://github.com/AgoraIO/signaling-sdk-samples-unity + ``` + +1. **Open the reference app in Unity Editor**: + + 1. In Unity Hub, Open `signaling-sdk-samples-unity`, Unity Editor opens the project. + + Unity Editor warns of compile errors. Don't worry, you fix them when you import Signaling SDK for Unity. + + 1. Go `Assets\Scenes`, and open `SampleScene.unity`. The sample scene opens. + + 1. Download and extract the latest version of the [Agora Signaling SDK](https://docs.agora.io/en/sdks?platform=unity) to a local folder. + + 1. In **Unity**, click **Assets** > **Import Package** > **Custom Package**. + + 1. Navigate to the Signaling SDK package and click **Open**. + + 1. In **Import Unity Package**, click **Import**. + + Unity recompiles the Signaling SDK samples for Unity and the warnings disappear. + + \ No newline at end of file diff --git a/shared/common/project-setup/web.mdx b/shared/common/project-setup/web.mdx index ac44dec78..05de231ca 100644 --- a/shared/common/project-setup/web.mdx +++ b/shared/common/project-setup/web.mdx @@ -1,37 +1,32 @@ - + +1. **Clone the [ reference app](https://github.com/AgoraIO/video-sdk-samples-js) repository**. -1. **Clone the repository**: + Navigate to your `` folder and run the following command: - To clone the repository to your local machine, open Terminal and navigate to the directory where you want to clone the repository. Then, use the following command: - ```bash - git clone https://github.com/AgoraIO/signaling-sdk-samples-web - ``` + ```bash + git clone https://github.com/AgoraIO/video-sdk-samples-js.git + ``` -1. **Install the dependencies**: +1. **Install dependencies**: Navigate to the root directory of the cloned repository and run the following command: ```bash pnpm install ``` - - By default is installed automatically. However, you can also [Install manually](../reference/downloads#through-the-agora-website). - - - -1. **Clone the reference app to `` on your development environment**: + +1. **Clone the [ reference app](https://github.com/AgoraIO/signaling-sdk-samples-web) repository**. Navigate to your `` folder and run the following command: - - ```bash - git clone https://github.com/AgoraIO/video-sdk-samples-js.git - ``` + ```bash + git clone https://github.com/AgoraIO/signaling-sdk-samples-web + ``` 1. **Install dependencies**: @@ -40,7 +35,6 @@ ```bash pnpm install ``` - diff --git a/shared/common/project-setup/windows.mdx b/shared/common/project-setup/windows.mdx index 25f0919d8..4a8d55a34 100644 --- a/shared/common/project-setup/windows.mdx +++ b/shared/common/project-setup/windows.mdx @@ -1,40 +1,43 @@ - -1. **Clone the reference app to `` on your development environment**: + - Navigate to your `` folder and run the following command: - +1. Clone this Git repository for sample code for windows by executing the following command in a terminal window: ```bash - git clone https://github.com/AgoraIO/video-sdk-samples-windows.git + git clone https://github.com/AgoraIO/video-sdk-samples-windows ``` - - - - - -1. **Open the project** - 1. Double-click `video-sdk-samples-windows/agora_manager/agora_manager.sln`. The project opens in Visual Studio. - 1. Unzip [Video SDK for Windows](https://docs.agora.io/en/sdks?platform=windows) to a local directory. - 1. Replace `video-sdk-samples-windows/agora_manager/sdk` with the contents of - `Agora_Native_SDK_for_Windows_FULL\sdk` - 1. In `video-sdk-samples-windows/agora_manager` create a folder using the following naming convention: ``/``. For example, for a debug profile on a 64 bit development device: `x64/Debug` - 1. Copy the contents of `video-sdk-samples-windows/agora_manager/SDK/` to `video-sdk-samples-windows/agora_manager//`. - -1. **Install the required third party libraries** - - 1. In the command prompt, navigate to `video-sdk-samples-windows`. - 1. Install `vcpkg`: - ```bash - 1. git clone https://github.com/Microsoft/vcpkg.git - 1. cd vcpkgx - 1. .\bootstrap-vcpkg.bat - ``` - 1. Install the required packages: - ```bash - .\vcpkg.exe install jsoncpp:x64-windows - .\vcpkg.exe install curl:x64-windows - .\vcpkg.exe install opencv:x64-windows - `` +1. Replace the `video-sdk-samples-windows/agora_manager/SDK` with the latest Agora Video SDK, you downloaded and unzipped to a local folder. +1. Create a folder at your solution directory (`video-sdk-samples-windows/agora_manager`) named `/` like for this sample create a nested folder (`x64/Debug`).To achieve this, first create a folder named `x64`, and then within that folder, create another folder named `Debug`. +1. Copy the contents of `video-sdk-samples-windows/agora_manager/SDK/x86_64` to `video-sdk-samples-windows/agora_manager/x64/Debug`. + +1. Install the required third-party libraries + 1. Install `vcpkg`: - + Open command prompt, navigate to `video-sdk-samples-windows` and run the following commands: + + ```bash + git clone https://github.com/Microsoft/vcpkg.git + cd vcpkg + .\bootstrap-vcpkg.bat + ``` + + 1. Install required packages : + + Make sure you install x64-windows version of the libraries. + + ```bash + .\vcpkg.exe install jsoncpp:x64-windows + .\vcpkg.exe install curl:x64-windows + .\vcpkg.exe install opencv:x64-windows + ``` + 1. Ensure `vcpkg` integration with Visual Studio: + + Make sure you have integrated `vcpkg` with Visual Studio. After cloning `vcpkg` and installing the libraries, run the integration command: + + ```bash + .\vcpkg.exe integrate install + ``` + + The integration command shows a confirmation message upon successful integration with Visual Studio. This integration is a one-time requirement for a specific system, regardless of the number of repositories created therein. + + \ No newline at end of file diff --git a/shared/common/project-test/android.mdx b/shared/common/project-test/android.mdx index 5f708c966..21d0343ff 100644 --- a/shared/common/project-test/android.mdx +++ b/shared/common/project-test/android.mdx @@ -1,40 +1,5 @@ - - -1. **Set the AppID** - - In `/signaling-manager/src/main/res/raw/config.json`, set `appId` to the [AppID](../reference/manage-agora-account#get-the-app-id) of your project. - - - In `/signaling-manager/src/main/res/raw/config.json`, set `appId` to the [AppID](../reference/manage-agora-account#get-the-app-id) of your project. - - -1. **Set the authentication method** - - Choose one of the following authentication methods: - - - **Temporary token**: - 1. Set `token` with the value of your [temporary token](https://agora-token-generator-demo.vercel.app/). - 1. Set `channelName` - with the name of a channel you used to create the token. - - **Authentication server**: - 1. Setup an [Authentication server](../get-started/authentication-workflow?platform#create-and-run-a-token-server) - 1. In `config.json`, set: - - `channelName` with the name of a channel you want to join. - - `token` to an empty string. - - `serverUrl` - with the base URL for your token server. For example: `https://agora-token-service-production-yay.up.railway.app`. - -1. **Run the reference app**: - - 1. In Android Studio, connect a physical Android device to your development machine. - 1. Click Run to start the app. - 1. A moment later you see the project installed on your device. - 1. From the main screen of the app, select **SDK Quickstart**. Use the URL displayed in Terminal to open the app in your browser. - - - - - -3. **Set the APP ID** +4. **Set the APP ID** In `agora-manager/res/raw/config.json`, set `appId` to the [AppID](../reference/manage-agora-account?platform=android#get-the-app-id) of your project. @@ -60,7 +25,4 @@ A moment later you see the project installed on your device. If this is the first time you run the project, you need to grant microphone and camera access to your . - - - diff --git a/shared/common/project-test/clone-project.mdx b/shared/common/project-test/clone-project.mdx new file mode 100644 index 000000000..965301233 --- /dev/null +++ b/shared/common/project-test/clone-project.mdx @@ -0,0 +1,31 @@ + + - documentation app + + + - documentation app + + + - documentation app + + + - documentation app + + + - documentation app + + + - documentation app + + + - documentation app + + + - documentation app + + + - documentation app + + + - documentation app + + diff --git a/shared/common/project-test/electron.mdx b/shared/common/project-test/electron.mdx index 23cb0aff0..c6bd811f2 100644 --- a/shared/common/project-test/electron.mdx +++ b/shared/common/project-test/electron.mdx @@ -6,7 +6,7 @@ 2. Execute the following command in the terminal: - ``` bash + ```bash git clone https://github.com/electron/electron-quick-start ``` This command clones the Electron quick-start project that you use to implement . @@ -15,7 +15,7 @@ Open a terminal window in your project folder and execute the following command to download and install the . - ``` bash + ```bash npm i agora-electron-sdk ``` Make sure the path to your project folder does not contain any spaces. This might cause error during the installation. diff --git a/shared/common/project-test/generate-temp-rtc-token.mdx b/shared/common/project-test/generate-temp-rtc-token.mdx new file mode 100644 index 000000000..e1724ad32 --- /dev/null +++ b/shared/common/project-test/generate-temp-rtc-token.mdx @@ -0,0 +1 @@ +Generate a temporary token in diff --git a/shared/common/project-test/index.mdx b/shared/common/project-test/index.mdx index 2d8fac272..b03b6664a 100644 --- a/shared/common/project-test/index.mdx +++ b/shared/common/project-test/index.mdx @@ -8,7 +8,13 @@ import Electron from './electron.mdx'; import Flutter from './flutter.mdx'; import Unity from './unity.mdx'; import Windows from './windows.mdx'; +import CloneProj from './clone-project.mdx' + + + + + diff --git a/shared/common/project-test/load-web-demo.mdx b/shared/common/project-test/load-web-demo.mdx new file mode 100644 index 000000000..5f3b3217a --- /dev/null +++ b/shared/common/project-test/load-web-demo.mdx @@ -0,0 +1 @@ +In your browser, navigate to the web demo and update _App ID_, _Channel_, and _Token_ with the values for your temporary token, then click **Join**. diff --git a/shared/common/project-test/macos.mdx b/shared/common/project-test/macos.mdx index 74e608cdd..38ff7300d 100644 --- a/shared/common/project-test/macos.mdx +++ b/shared/common/project-test/macos.mdx @@ -1,6 +1,5 @@ import Source from './swift.mdx'; - diff --git a/shared/common/project-test/open-config-file.mdx b/shared/common/project-test/open-config-file.mdx index 5d115a9af..ce67bb083 100644 --- a/shared/common/project-test/open-config-file.mdx +++ b/shared/common/project-test/open-config-file.mdx @@ -1,3 +1,4 @@ + Open the file `/signaling-manager/src/main/res/raw/config.json` @@ -9,3 +10,38 @@ Open the file `/src/signaling_manager/config.json` + + + Open the file `/Assets/utils/Config.json` + + + Open the file `/config.json` + + + + + + Open the file `/agora-manager/res/raw/config.json` + + + + Open the file `DocsAppConfig.swift` + + + + Open the file `/src/agora_manager/config.json` + + + + Open the file `/Assets/agora-manager/config.json` + + + + Open the file `/src/agora-manager/config.json` + + + + Open the file `/video-sdk-samples-windows/agora-manager/config.json` + + + \ No newline at end of file diff --git a/shared/common/project-test/react-js.mdx b/shared/common/project-test/react-js.mdx index e501b4c18..aa1e6edad 100644 --- a/shared/common/project-test/react-js.mdx +++ b/shared/common/project-test/react-js.mdx @@ -13,9 +13,8 @@ - **Authentication server**: 1. Setup an [Authentication server](../get-started/authentication-workflow?platform#create-and-run-a-token-server) 1. In `config.json`: - - 1. Set `rtcToken` to an empty string. - 1. Set `serverUrl` to the base URL of your authentication server. For example, `https://agora-token-service-production-1234.up.railway.app`. + - Set `rtcToken` to an empty string. + - Set `serverUrl` to the base URL of your authentication server. For example, `https://agora-token-service-production-1234.up.railway.app`. 1. Start a proxy server so this web app can make HTTP calls to fetch a token. In a Terminal instance in the reference app root, run the following command: ```bash @@ -26,19 +25,16 @@ In Terminal, run the following command: - ``` bash + ```bash yarn dev ``` -1. **Open the project in your browser**. +1. **Open the project in your browser**: The default URL is http://localhost:5173/. -1. **Test ** - - In the dropdown, select this document and test . - - +1. **Test **: + In **Choose a product**, select . diff --git a/shared/common/project-test/react-native.mdx b/shared/common/project-test/react-native.mdx index ee3e9eea4..e45dcb562 100644 --- a/shared/common/project-test/react-native.mdx +++ b/shared/common/project-test/react-native.mdx @@ -1,35 +1,61 @@ -3. **Set the APP ID** +1. **Setup a React Native environment for project** - In `src/agora-manager/config.ts`, set `appId` to the [AppID](../reference/manage-agora-account?platform=android#get-the-app-id) of your project. + In the terminal, run the following command: -1. **Set the authentication method** + ```bash + npx react-native init ProjectName --template react-native-template-typescript + ``` - Choose one of the following authentication methods: + npx creates a new boilerplate project in the `ProjectName` folder. - - **Temporary token**: - 1. Set `rtcToken` with the value of your [temporary token](../reference/manage-agora-account#generate-a-temporary-token). - 1. Set `channelName` - with the name of a channel you used to create the token. - - **Authentication server**: - 1. Setup an [Authentication server](../get-started/authentication-workflow?platform#create-and-run-a-token-server) - 1. In `config.json`, set: - - `channelName` with the name of a channel you want to join. - - `rtcToken` to an empty string. - - `serverUrl` to the base URL of your authentication server. For example, `https://agora-token-service-production-1234.up.railway.app`. + For Android projects, enable the project to use Android SDK. In the `android` folder of your project, set the `sdk.dir` in the `local.properties` file. For example: -1. **Start the reference app** - - iOS - ```bash - yarn run ios - ``` - - Android + ```bash + sdk.dir=C:\\PATH\\TO\\ANDROID\\SDK + ``` - ```bash - yarn run android - ``` - A moment later you see the project installed on your device. If this is the first time you run the project, you need to grant microphone and camera access to your . +1. **Test the setup** + Launch your Android or iOS simulator and run your project by executing the following command: + + 1. Run `npx react-native start` in the root of your project to start Metro. + 1. Open another terminal in the root of your project and run `npx react-native run-android` to start the Android app, or run `npx react-native run-ios` to start the iOS app. + + You see your new app running in your Android or iOS simulator. You can also run your project on a physical Android or iOS device. For detailed instructions, see [Running on device](https://reactnative.dev/docs/running-on-device). + +1. **Integrate and configure ** + + To integrate on React Native 0.60.0 or later: + 1. Navigate to the root folder of your project in the terminal and integrate with either: + - npm + + ```bash + npm i --save react-native-agora + ``` + + - yarn + + ```bash + // Install yarn. + npm install -g yarn + // Download the Agora React Native SDK using yarn. + yarn add react-native-agora + ``` + + Do not link native modules manually, React Native 0.60.0 and later support [Autolinking](https://github.com/react-native-community/cli/blob/main/docs/autolinking.md). + + 1. If your target platform is iOS, use CocoaPods to install : + + ```bash + npx pod-install + ``` + + 1. uses Swift in native modules, your project must support compiling Swift. To create `File.swift`: + + 1. In Xcode, open `ios/ProjectName.xcworkspace`. + 1. Click **File > New > File, select iOS > Swift File**, then click **Next > Create** . \ No newline at end of file diff --git a/shared/common/project-test/rtc-first-steps.mdx b/shared/common/project-test/rtc-first-steps.mdx new file mode 100644 index 000000000..074984bbd --- /dev/null +++ b/shared/common/project-test/rtc-first-steps.mdx @@ -0,0 +1,26 @@ +import OpenConfig from '@docs/shared/common/project-test/open-config-file.mdx'; +import SetAppId from '@docs/shared/common/project-test/set-app-id.mdx'; +import SetAuthenticationMethod from '@docs/shared/common/project-test/set-authentication-rtc.mdx'; +import RunApp from '@docs/shared/common/project-test/run-reference-app.mdx'; +import GetTempToken from '@docs/shared/common/project-test/generate-temp-rtc-token.mdx'; +import LoadWebDemo from '@docs/shared/common/project-test/load-web-demo.mdx'; +import CloneProj from './clone-project.mdx' + +1. **Load the web demo** + + 1. + 1. + +1. **Clone the documentation reference app** + + + +1. **Configure the project** + + 1. + 1. + 1. + +1. **Run the reference app** + + diff --git a/shared/common/project-test/run-reference-app.mdx b/shared/common/project-test/run-reference-app.mdx index 6c62ec8eb..3a96aef18 100644 --- a/shared/common/project-test/run-reference-app.mdx +++ b/shared/common/project-test/run-reference-app.mdx @@ -1,10 +1,14 @@ 1. In Android Studio, connect a physical Android device to your development machine. - 1. Click Run to start the app. + 1. Click **Run** to launch the app. 1. A moment later you see the project installed on your device. + + + + In Terminal, navigate to ``, then run the following command: @@ -13,4 +17,53 @@ ``` Use the URL displayed in the terminal to open the in your browser. - \ No newline at end of file + + + + In Terminal, run the following command: + + ```bash + yarn dev + ``` + + + + In Unity, click **Play**. You see the game running on your device. + + + + Install the cURL library using the following command: + + ```bash + sudo apt-get install libcurl4-openssl-dev + ``` + + To build the project, open Terminal and run the following command in the project folder: + + ```bash + sh +x clean_build.sh + ``` + To run the project, execute: + + ```bash + ./build/SignalingDemo + ``` + + + + 1. Double click on `/video-sdk-samples-windows/agora_manager/agora_manager.sln`. It will open the solution in Visual Studio. + + 1. Select the project you want to run(`get_started`) in solution explorer, right click and `set as startup project' + + 1. Build and run the project. + + A moment later you see the project installed on your device. + + + + If this is the first time you run the project, grant microphone and camera access to the app. + + + If this is the first time you run the project, grant microphone access to the app. + + diff --git a/shared/common/project-test/set-app-id.mdx b/shared/common/project-test/set-app-id.mdx new file mode 100644 index 000000000..a28ab19d5 --- /dev/null +++ b/shared/common/project-test/set-app-id.mdx @@ -0,0 +1 @@ + Set `appId` to the [AppID](../reference/manage-agora-account#get-the-app-id) of your project. \ No newline at end of file diff --git a/shared/common/project-test/set-authentication-rtc.mdx b/shared/common/project-test/set-authentication-rtc.mdx new file mode 100644 index 000000000..575cbba57 --- /dev/null +++ b/shared/common/project-test/set-authentication-rtc.mdx @@ -0,0 +1,11 @@ + Choose one of the following authentication methods: + + - **Temporary token** + 1. [Generate an RTC token](https://agora-token-generator-demo.vercel.app/) using your `uid` and `channelName` and set `rtcToken` to this value in `config.json`. + 1. Set `channelName` to the name of the channel you used to create the `rtcToken`. + - **Authentication server** + 1. Setup an [Authentication server](../get-started/authentication-workflow?platform#create-and-run-a-token-server) + 1. In `config.json`, set: + - `channelName` to the name of a channel you want to join. + - `token` and `rtcToken` to empty strings. + - `serverUrl` to the base URL for your token server. For example: `https://agora-token-service-production-yay.up.railway.app`. \ No newline at end of file diff --git a/shared/common/project-test/swift.mdx b/shared/common/project-test/swift.mdx index 476165b15..d9f9fa4ef 100644 --- a/shared/common/project-test/swift.mdx +++ b/shared/common/project-test/swift.mdx @@ -1,7 +1,8 @@ -3. **Set the APP ID** + +4. **Set the APP ID** - In `DocsAppConfig.swift` set `appId` to the [AppID](../reference/manage-agora-account?platform=android#get-the-app-id) of your project. + In `DocsAppConfig.swift` set `appId` to the [AppID](../reference/manage-agora-account#get-the-app-id) of your project. 1. **Set the authentication method**: @@ -9,20 +10,15 @@ 1. Set `rtcToken` with the value of your [temporary token](../reference/manage-agora-account#generate-a-temporary-token). 1. Set `channelName` - with the name of a channel you used to create the token. - **Authentication server**: - 1. Setup an [Authentication server](../get-started/authentication-workflow?platform#create-and-run-a-token-server) + 1. Setup an [Authentication server](../get-started/authentication-workflow#create-and-run-a-token-server) 1. In `DocsAppConfig.swift`: 1. Set `rtcToken` to an empty string. 1. Set `serverUrl` to the base URL of your authentication server. For example, `https://agora-token-service-production-1234.up.railway.app`. -1. **Run the reference app**: +1. **Run the reference **: If this is the first time you run the project, grant microphone and camera access to your . -1. **Test ** - - In the dropdown, select this document and test . - - - - + + \ No newline at end of file diff --git a/shared/common/project-test/windows.mdx b/shared/common/project-test/windows.mdx index e21071ae0..fe9c3fa65 100644 --- a/shared/common/project-test/windows.mdx +++ b/shared/common/project-test/windows.mdx @@ -3,7 +3,7 @@ 3. **Set the APP ID** - In `video-sdk-samples-windows/config.json`, set `appId` to the [AppID](../reference/manage-agora-account?platform=android#get-the-app-id) of your project. + In `video-sdk-samples-windows/agora-manager/config.json`, set `appId` to the [AppID](../reference/manage-agora-account?platform=android#get-the-app-id) of your project. 4. **Set the authentication method** @@ -14,15 +14,14 @@ 1. Set `channelName` - with the name of a channel you used to create the token. - **Authentication server**: 1. Setup an [Authentication server](../get-started/authentication-workflow?platform#create-and-run-a-token-server) - 1. In `config.json`, set: + 1. In `video-sdk-samples-windows/agora-manager/config.json`, set: - `channelName` with the name of a channel you want to join. - `rtcToken` to an empty string. - `serverUrl` to the base URL of your authentication server. For example, `https://agora-token-service-production-1234.up.railway.app`. 5. **Run the sample** - 1. Double-click on `video-sdk-samples-windows/agora_manager/agora_manager.sln`. It will open the solution in - Visual Studio. + 1. Double click on `video-sdk-samples-windows/agora_manager/agora_manager.sln`. It will open the solution in Visual Studio. 1. Select the project you want to run in solution explorer, right click and `set as startup project' diff --git a/shared/common/security/_security-practice.mdx b/shared/common/security/_security-practice.mdx index c87aa39ca..3d31c5059 100644 --- a/shared/common/security/_security-practice.mdx +++ b/shared/common/security/_security-practice.mdx @@ -26,7 +26,7 @@ A token is generated with important information such as App ID, user ID (`uid`), ![1663232522276](https://web-cdn.agora.io/docs-files/1663232522276) -The app developer can enable token authentication (App Certificate) on [Console](https://console.agora.io/). When enabled, all user’s request to join a channel must be done with a valid token. +The app developer can enable token authentication (App Certificate) on . When enabled, all user’s request to join a channel must be done with a valid token. ![img](/images/certificate-enable.png) @@ -65,7 +65,7 @@ These SDKs support network geofencing in the following regions: global (default) Use this list to quickly check what measures you have or have not taken to best protect the security of you app and users: -1. [Enable token authentication](../reference/manage-agora-account#manage-app-certificates) on Agora [Console](https://console.agora.io/). +1. [Enable token authentication](../reference/manage-agora-account#manage-app-certificates) on . 2. Disable *No certificate* in your project management page. Once it is done your app authenticates users with tokens only. 3. [Deploy a token server](/video-calling/get-started/authentication-workflow) in your backend services. 4. Protect the token server and only allow the app backend server to connect to the token server. diff --git a/shared/extensions-marketplace/_develop-an-audio-filter.mdx b/shared/extensions-marketplace/_develop-an-audio-filter.mdx index b17b0f724..6dd4738cb 100644 --- a/shared/extensions-marketplace/_develop-an-audio-filter.mdx +++ b/shared/extensions-marketplace/_develop-an-audio-filter.mdx @@ -11,8 +11,7 @@ import ProjectTest from '@docs/shared/extensions-marketplace/common/_project-tes The audio filters you created are easily integrated into apps to supply your voice effects and noise cancellation. - - + ## Understand the tech @@ -64,5 +63,4 @@ To ensure that you have integrated the extension in your : This section contains information that completes the information in this page, or points you to documentation that explains other aspects to this product. - \ No newline at end of file diff --git a/shared/extensions-marketplace/_superclarity.mdx b/shared/extensions-marketplace/_superclarity.mdx deleted file mode 100644 index 28f302336..000000000 --- a/shared/extensions-marketplace/_superclarity.mdx +++ /dev/null @@ -1,118 +0,0 @@ -import Prerequisites from '@docs/shared/extensions-marketplace/common/_prerequities.mdx'; - -This extension provides the video feature for . - - - -## Understand the tech - -To use the extension in your project, you create an extension instance and register it. After you create an `ILocalVideoTrack` or `IRemoteVideoTrack`, you create a processor and pipe it to track. - -## Prerequisites - -The development environment requirements are as follows: - - - -- Agora Video SDK for Web (v4.16.1 or later) -- Implemented for . - - -## Project setup - -To implement features in your app, open the SDK quickstart for Video Calling project you created previously. - -1. Download and install the `agora-extension-super-clarity` package. - -1. Import these packages into your code. - - ```typescript - import AgoraRTC from "agora-rtc-sdk-ng"; - import { SuperClarityExtension, SuperClarityEvents } from "agora-extension-super-clarity"; - import type { ISCProcessor } from "agora-extension-super-clarity"; - ``` - -## Integrate the extension - -This section describes the call sequence you implement to use features in your app. - -1. Create an extension instance and register it. - - ```typescript - // Create an extension instance - const extension = new SuperClarityExtension(); - // Register the instance - AgoraRTC.registerExtensions([extension]); - ``` - -1. Create a processor and pipe your `ILocalVideoTrack` or `IRemoteVideoTrack` to it. - - ```typescript - // Create a processor - const processor = extension.createProcessor(); - processor.on(SuperClarityEvents.ERROR, (msg) => { - console.error("processor error", msg); - }); - processor.on(SuperClarityEvents.FIRST_VIDEO_FRAME, (msg) => { - console.log("processor first video frame", msg); - }); - processor.on(SuperClarityEvents.SKIPFRAME, (msg) => { - console.warning("processor skip one frame", msg); - }); - processor.on(SuperClarityEvents.STATS, (msg) => { - console.log("processor stats info", msg); - }); - - // Pipe the track to the processor - track.pipe(processor).pipe(track.processorDestination); - ``` - -1. Enable or disable the processor. - - ```typescript - // Enable the processor - const enable = async () => { - const enabled = processor.enabled; - if (!enabled) { - await processor.enable(); - } - }; - - // Disable the processor - const disable = async () => { - const enabled = processor.enabled; - if (enabled) { - await processor.disable(); - } - }; - ``` - -1. Unpipe and release the processor when it is no longer required. - - ```typescript - // Unpipe and release the processor resources - const release = async () => { - processor.unpipe(); - track.unpipe(); - track.pipe(track.processorDestination); - await processor.release(); - } - ``` - -## Reference - -This section contains content that completes the information in this page, or points you to documentation that explains other aspects to this product. - -### API reference - -- registerExtensions - -- pipe - -- unpipe - - - - -**This extension is currently not available for **. - \ No newline at end of file diff --git a/shared/extensions-marketplace/_use-an-extension.mdx b/shared/extensions-marketplace/_use-an-extension.mdx index 055218c1f..a3c2743b5 100644 --- a/shared/extensions-marketplace/_use-an-extension.mdx +++ b/shared/extensions-marketplace/_use-an-extension.mdx @@ -25,18 +25,13 @@ An extension accesses voice and video data when it is captured from the user's l A typical transmission pipeline consists of a chain of procedures, including capture, pre-processing, encoding, transmitting, decoding, post-processing, and play. Audio or video extensions are inserted into either the pre-processing or post-processing procedure, in order to modify the voice or video data in the transmission pipeline. - -**This functionality is not supported for Unreal Engine.** - - -**Coming soon for this beta program.** + +**Not yet available for .** - ## Prerequisites -In order to follow this procedure you must have: @@ -53,7 +48,7 @@ In order to integrate an extension into your project: 1. **Activate an extension** - 1. Log in to [](https://console.agora.io/). + 1. Log in to . 2. In the left navigation panel, click **Extension Marketplace**, then click the extension you want to activate. You are now on the extension detail page. diff --git a/shared/extensions-marketplace/activefence/index.mdx b/shared/extensions-marketplace/activefence/index.mdx index d52487852..3b5d39fc2 100644 --- a/shared/extensions-marketplace/activefence/index.mdx +++ b/shared/extensions-marketplace/activefence/index.mdx @@ -3,24 +3,18 @@ import Prerequisites from '@docs/shared/extensions-marketplace/common/_prerequit import Implementation from './project-implementation/index.mdx'; import Reference from './reference/index.mdx'; -[](https://www.activefence.com/) is the leader in providing Trust & Safety solutions to protect online platforms and their users from -malicious behavior and content. By integrating capabilities in your , design the -exact content moderation processes you want with the ActiveFence AI-driven automated detection for text, -audio, image and video content across multiple abuse areas and languages. Quickly moderate, enforce policies, manage -user flags, send notifications, and more. +[](https://www.activefence.com/) is the leader in providing Trust & Safety solutions to protect online platforms and their users from malicious behavior and content. By integrating capabilities in your , you can design the exact content moderation solution you need. Content moderation is powered by ActiveFence AI-driven automated detection for text, audio, image, and video content that enables you to moderate, enforce policies, manage user flags, and send notifications across multiple abuse areas and languages. **This extension is not supported for this platform.** -The following figure shows the workflow for to moderate content sent by a specific user to a channel: +The following figure shows the workflow to moderate content sent by a specific user to a channel: ![rtc-channel](/images/extensions-marketplace/active-fence.svg) - -This page shows you how to integrate and use the content detection and moderation extension in your -. +This page shows you how to integrate and use the content detection and moderation extension in your . ## Prerequisites @@ -28,11 +22,7 @@ The development environment requirements are as follows: - -- Implemented for . - - - + - Implemented for . @@ -90,6 +80,31 @@ In order to configure : 1. communicates any extra credentials to you. +1. **Customize your callback fields** + + allows you to add custom fields for your callbacks. Take the following steps to add custom callback fields: + + 1. In , open **Account Settings** by clicking the **Settings** icon on the top-right. + + 1. Click **Moderation Capabilities** > **Custom Fields**. + + 1. Press **Add Field** and choose **EDITABLE** to open an **Add an editable field** window. + + ![Add an editable field](/images/extensions-marketplace/active-fence-add-an-editable-field.png) + + 1. Repeat the step multiple times to add the following fields: + + | Title & Key | Type | Meaning | + |:------------|:-----|:--------| + | cname | Text | RTC channel name | + | requestId | Text | Request Id of the screenshot | + | sid | Text | Session Id | + | source | Text | Source of screenshot (agora) | + | timestamp | Number | Timestamp, For example, 20230614050206430 | + | uid | Number | User id (unique in a channel) | + + See [ docs](https://docs.activefence.com/#section/Integrating-with-the-TandS-Platform-How-To/Custom-Fields) for more information about custom fields. + 1. **Configure Action webhooks** 1. In , click **Data Management** > **Webhook Key Management**, then click **Add Key**. @@ -170,7 +185,6 @@ In order to configure : ## Integrate the extension - diff --git a/shared/extensions-marketplace/ai-noise-suppression.mdx b/shared/extensions-marketplace/ai-noise-suppression.mdx index ff79e7a18..93dfd282d 100644 --- a/shared/extensions-marketplace/ai-noise-suppression.mdx +++ b/shared/extensions-marketplace/ai-noise-suppression.mdx @@ -1,7 +1,9 @@ +import {PlatformWrapper} from "../../../src/mdx-components/PlatformWrapper"; import Prerequisites from '@docs/shared/common/prerequities.mdx'; import ProjectImplement from '@docs/shared/extensions-marketplace/ai-noise-suppression/project-implementation/index.mdx'; import Reference from '@docs/shared/extensions-marketplace/ai-noise-suppression/reference/index.mdx'; +import ProjectTest from '@docs/shared/extensions-marketplace/ai-noise-suppression/project-test/poc3.mdx'; Agora's , enables you to suppress hundreds of types of noise and reduce distortion in human voices when multiple people speak at the same time. In scenarios such as online meetings, online chat rooms, video consultations with doctors, and online gaming, makes virtual communication as smooth as face-to-face interaction. @@ -30,15 +32,7 @@ In the pre-processing stage, uses deep learning noise reductio ![](/images/extensions-marketplace/ai-noise-suppression.png) - - **This functionality is not supported for Unreal Engine.** - - -**Coming soon for this beta program.** - - - - + ## Prerequisites @@ -47,10 +41,9 @@ In the pre-processing stage, uses deep learning noise reductio ## Implementation -This section explains how to use the latest version of the extension. Implementation for previous versions might be different. For details, see the [release notes](/extensions-marketplace/reference/release-notes#ai-noise-suppression). +This section explains how to use the latest version of the extension. Implementation for previous versions might be different. For details, see the [release notes](/extensions-marketplace/overview/release-notes#ai-noise-suppression). - - + To activate in your and set the noise reduction mode, call: @@ -111,29 +104,28 @@ To activate in your and set the noise redu - + + + + When is enabled, if detects that the device performance is not sufficient, it: - Disables - Enables traditional noise reduction -- Throws the -1054(WARN_APM_AINS_CLOSED) error code. - +- Throws the `WARN_APM_AINS_CLOSED` (-1054) error code. - - -## Reference + -This section contains in-depth technical information about . +## Reference +This section completes the information on this page, or points you to documentation that explains other aspects about this product. -### API reference - diff --git a/shared/extensions-marketplace/ai-noise-suppression/project-implementation/index.mdx b/shared/extensions-marketplace/ai-noise-suppression/project-implementation/index.mdx index 066f07a63..b4e756a8a 100644 --- a/shared/extensions-marketplace/ai-noise-suppression/project-implementation/index.mdx +++ b/shared/extensions-marketplace/ai-noise-suppression/project-implementation/index.mdx @@ -1,19 +1,11 @@ -import Android from './android.mdx'; -import Ios from './ios.mdx'; -import Web from './web.mdx'; +import Poc3 from './poc3.mdx'; import Electron from './electron.mdx'; import Flutter from './flutter.mdx'; -import Unity from './unity.mdx'; -import MacOs from './macos.mdx' import Windows from './windows.mdx' import ReactNative from './react-native.mdx' - + - - - - diff --git a/shared/extensions-marketplace/ai-noise-suppression/project-implementation/poc3.mdx b/shared/extensions-marketplace/ai-noise-suppression/project-implementation/poc3.mdx new file mode 100644 index 000000000..d548432b4 --- /dev/null +++ b/shared/extensions-marketplace/ai-noise-suppression/project-implementation/poc3.mdx @@ -0,0 +1,45 @@ +import ImportLibrary from '@docs/assets/code/video-sdk/ai-noise-suppression/import-library.mdx'; +import EnableDenoiser from '@docs/assets/code/video-sdk/ai-noise-suppression/enable-denoiser.mdx'; +import SetupLogging from '@docs/assets/code/video-sdk/ai-noise-suppression/setup-logging.mdx'; +import ImportPlugin from '@docs/assets/code/video-sdk/ai-noise-suppression/import-plugin.mdx'; +import ConfigureExtension from '@docs/assets/code/video-sdk/ai-noise-suppression/configure-extension.mdx'; +import SetMode from '@docs/assets/code/video-sdk/ai-noise-suppression/set-noise-reduction-mode.mdx'; +import SetLevel from '@docs/assets/code/video-sdk/ai-noise-suppression/set-reduction-level.mdx'; + + + +### Import the library + + +### Enable the denoiser + + + +### Setup logging + + + + + + + + +### Import the plugin + + + + +### Enable AI noise suppression + + + + +### Add the required imports + +### Configure the AI noise suppression extension + +### Set the noise reduction mode + +### Set the noise reduction level + + diff --git a/shared/extensions-marketplace/ai-noise-suppression/project-implementation/web.mdx b/shared/extensions-marketplace/ai-noise-suppression/project-implementation/web.mdx index 42f029150..91966c426 100644 --- a/shared/extensions-marketplace/ai-noise-suppression/project-implementation/web.mdx +++ b/shared/extensions-marketplace/ai-noise-suppression/project-implementation/web.mdx @@ -28,7 +28,7 @@ 2. If you have enabled the Content Security Policy (CSP), because Wasm files are not allowed to load in Chrome and Edge by default, you need to configure the CSP as follows: - For versions later than Chrome 97 and Edge 97 (Chrome 97 and Edge 97 included): Add `'wasm-unsafe-eval'` and `blob:` in the [`script-src`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src) options. For example: - ``` xml + ```xml ``` diff --git a/shared/extensions-marketplace/ai-noise-suppression/project-test/poc3.mdx b/shared/extensions-marketplace/ai-noise-suppression/project-test/poc3.mdx new file mode 100644 index 000000000..c1412810d --- /dev/null +++ b/shared/extensions-marketplace/ai-noise-suppression/project-test/poc3.mdx @@ -0,0 +1,22 @@ +import TestFirstSteps from '@docs/shared/common/project-test/rtc-first-steps.mdx'; +import ReactJS from './react-js.mdx'; + + + +## Test AI noise suppression + + + + + +5. **Join a channel** + +6. **Test noise suppression** + + Talk to the remote user connected to the web demo. Turn noise suppression on or off to see the effect of this feature. + + + + + + \ No newline at end of file diff --git a/shared/extensions-marketplace/ai-noise-suppression/project-test/react-js.mdx b/shared/extensions-marketplace/ai-noise-suppression/project-test/react-js.mdx new file mode 100644 index 000000000..2cded4a0c --- /dev/null +++ b/shared/extensions-marketplace/ai-noise-suppression/project-test/react-js.mdx @@ -0,0 +1,23 @@ + + +5. **Choose this sample in the reference app** + + In **Choose a sample code**, select **AI noise suppression**. + +1. **Join a channel** + + + Click **Join** to start a session. When you select **Host**, the local video is published and played in the . When you select **Audience**, the remote stream is subscribed and played. + + + + Press **Join** to connect to the same channel as your web demo. + + +1. **Test the AI suppression extension** + + 1. In **Noise reduction mode**, select a noise reduction mode. + 1. In **Noise reduction level**, select a noise reduction level. + + Now, observe the impact of the chosen noise reduction mode and level on the noise in your app. + \ No newline at end of file diff --git a/shared/extensions-marketplace/ai-noise-suppression/reference/index.mdx b/shared/extensions-marketplace/ai-noise-suppression/reference/index.mdx index 066f07a63..b6492a81a 100644 --- a/shared/extensions-marketplace/ai-noise-suppression/reference/index.mdx +++ b/shared/extensions-marketplace/ai-noise-suppression/reference/index.mdx @@ -8,6 +8,7 @@ import MacOs from './macos.mdx' import Windows from './windows.mdx' import ReactNative from './react-native.mdx' + diff --git a/shared/extensions-marketplace/ai-noise-suppression/reference/ios.mdx b/shared/extensions-marketplace/ai-noise-suppression/reference/ios.mdx index 72231ddd6..52ad025f4 100644 --- a/shared/extensions-marketplace/ai-noise-suppression/reference/ios.mdx +++ b/shared/extensions-marketplace/ai-noise-suppression/reference/ios.mdx @@ -1,5 +1,3 @@ -setParameters - diff --git a/shared/extensions-marketplace/ai-noise-suppression/reference/macos.mdx b/shared/extensions-marketplace/ai-noise-suppression/reference/macos.mdx index bfa28c097..5b1a67bc4 100644 --- a/shared/extensions-marketplace/ai-noise-suppression/reference/macos.mdx +++ b/shared/extensions-marketplace/ai-noise-suppression/reference/macos.mdx @@ -1,5 +1,3 @@ -setParameters - diff --git a/shared/extensions-marketplace/ai-noise-suppression/reference/web.mdx b/shared/extensions-marketplace/ai-noise-suppression/reference/web.mdx index e2e907d54..b9c89c0cb 100644 --- a/shared/extensions-marketplace/ai-noise-suppression/reference/web.mdx +++ b/shared/extensions-marketplace/ai-noise-suppression/reference/web.mdx @@ -1,4 +1,9 @@ + +- For a working example, check out the [AI Denoiser web demo](https://webdemo.agora.io/aiDenoiser/index.html) and the associated [source code](https://github.com/AgoraIO/API-Examples-Web/tree/main/Demo/aiDenoiser). + +### API reference + #### IAIDenoiserExtension ##### checkCompatibility diff --git a/shared/extensions-marketplace/common/_prerequities.mdx b/shared/extensions-marketplace/common/_prerequities.mdx index ab2989784..a4c370d77 100644 --- a/shared/extensions-marketplace/common/_prerequities.mdx +++ b/shared/extensions-marketplace/common/_prerequities.mdx @@ -18,7 +18,7 @@ - C# -- A [supported browser](../reference/supported-platforms#browsers). +- A [supported browser](../overview/supported-platforms#browsers). - Physical media input devices, such as a camera and a microphone. - A JavaScript package manager such as [npm](https://www.npmjs.com/package/npm). diff --git a/shared/extensions-marketplace/common/project-test/poc3.mdx b/shared/extensions-marketplace/common/project-test/poc3.mdx new file mode 100644 index 000000000..cf0b2892f --- /dev/null +++ b/shared/extensions-marketplace/common/project-test/poc3.mdx @@ -0,0 +1,14 @@ +import CloneReference from '@docs/shared/common/project-test/clone-project.mdx'; + + +1. [Generate a temporary token](../reference/manage-agora-account#generate-a-temporary-token) in if required. + +1. In your browser, navigate to the web demo and update _App ID_, _Channel_, and _Token_ with the values for your temporary token, then click **Join**. + + + +4. In the reference , update `appId` and `rtcToken` in `config.json` with the values from . + +5. Follow the steps in the project's README for setup. + + \ No newline at end of file diff --git a/shared/extensions-marketplace/develop-a-video-filter/project-implementation/cpp.mdx b/shared/extensions-marketplace/develop-a-video-filter/project-implementation/cpp.mdx index 9b6ac2f22..d2cc1d848 100644 --- a/shared/extensions-marketplace/develop-a-video-filter/project-implementation/cpp.mdx +++ b/shared/extensions-marketplace/develop-a-video-filter/project-implementation/cpp.mdx @@ -21,7 +21,7 @@ Methods include: The following code sample shows how to use these APIs together to implement a video filter: -``` cpp +```cpp #include "ExtensionVideoFilter.h" #include "../logutils.h" #include @@ -150,7 +150,7 @@ To encapsulate the video filter into an extension, you need to implement the `IE The following code sample shows how to use these APIs to encapsulate the video filter: -``` cpp +```cpp #include "ExtensionProvider.h" #include "../logutils.h" #include "VideoProcessor.h" diff --git a/shared/extensions-marketplace/develop-an-audio-filter/_web.mdx b/shared/extensions-marketplace/develop-an-audio-filter/_web.mdx index d711ba0d9..ae018f3b3 100644 --- a/shared/extensions-marketplace/develop-an-audio-filter/_web.mdx +++ b/shared/extensions-marketplace/develop-an-audio-filter/_web.mdx @@ -19,7 +19,7 @@ Agora provides the following abstract classes for developing an audio extension: Before proceeding, ensure that your development environment meets the following requirements: - A Windows or macOS computer that meets the following criteria: - - A browser that matches the [supported browser list](../reference/supported-platforms). Agora highly recommends using [the latest stable version]( https://www.google.com/chrome/) of Google Chrome. + - A browser that matches the [supported browser list](../overview/supported-platforms). Agora highly recommends using [the latest stable version]( https://www.google.com/chrome/) of Google Chrome. - Physical media input devices, such as a built-in camera and a built-in microphone. - Access to the Internet. If your network has a firewall, follow the instructions in [Firewall Requirements]( ../reference/firewall) to access Agora services. - An Intel 2.2GHz Core i3/i5/i7 processor (2nd generation) or equivalent diff --git a/shared/extensions-marketplace/develop-an-audio-filter/project-implementation/cpp.mdx b/shared/extensions-marketplace/develop-an-audio-filter/project-implementation/cpp.mdx index 90d6618b9..264ca5d62 100644 --- a/shared/extensions-marketplace/develop-an-audio-filter/project-implementation/cpp.mdx +++ b/shared/extensions-marketplace/develop-an-audio-filter/project-implementation/cpp.mdx @@ -19,7 +19,7 @@ Use the `IAudioFilter` interface to implement an audio filter. You can find the The following code sample shows how to use these methods together to implement an audio filter extension: -``` cpp +```cpp // After receiving the audio frames to be processed, call adaptAudioFrame to process the audio frames. bool ExtensionAudioFilter::adaptAudioFrame(const media::base::AudioPcmFrame &inAudioPcmFrame, media::base::AudioPcmFrame &adaptedPcmFrame) { @@ -66,7 +66,7 @@ To encapsulate the audio filter into an extension, you need to implement the `IE The following code sample shows how to use these methods to encapsulate the audio filter: -``` cpp +```cpp void ExtensionProvider::enumerateExtensions(ExtensionMetaInfo* extension_list, int& extension_count) { extension_count = 2; diff --git a/shared/extensions-marketplace/drm-play/project-implementation/swift.mdx b/shared/extensions-marketplace/drm-play/project-implementation/swift.mdx index 2234539a0..8f6c971f0 100644 --- a/shared/extensions-marketplace/drm-play/project-implementation/swift.mdx +++ b/shared/extensions-marketplace/drm-play/project-implementation/swift.mdx @@ -10,13 +10,13 @@ To create these buttons, in the `ViewController` class: Add the following lines along with the other declarations at the top: - ``` swift + ```swift var SearchMusic: UIButton! var PlayMusic: UIButton! ``` - ``` swift + ```swift var SearchMusic: NSButton! var PlayMusic: NSButton! ``` @@ -27,7 +27,7 @@ To create these buttons, in the `ViewController` class: Paste the following lines inside the `initViews` function: - ``` swift + ```swift SearchMusic = UIButton(type: .system) SearchMusic.frame = CGRect(x: 100, y: 550, width: 200, height: 50) SearchMusic.setTitle("Search Music", for: .normal) @@ -44,7 +44,7 @@ To create these buttons, in the `ViewController` class: ``` - ``` swift + ```swift SearchMusic = NSButton() SearchMusic.frame = CGRect(x: 255, y: 10, width: 150, height: 20) SearchMusic.title = "Start Channel Media Relay" diff --git a/shared/extensions-marketplace/dubbing-voice-changer/index.mdx b/shared/extensions-marketplace/dubbing-voice-changer/index.mdx index faa8d56ab..c0171f994 100644 --- a/shared/extensions-marketplace/dubbing-voice-changer/index.mdx +++ b/shared/extensions-marketplace/dubbing-voice-changer/index.mdx @@ -19,7 +19,7 @@ The `key` parameter you specify in the `setExtensionProperty` method corresponds -Using the setExtensionPropertyWithVendor method provided in SDK v and passing in the specified `key` and `value` parameters, you can quickly integrate real-time AI voice conversion ability into your . +Using the setExtensionPropertyWithVendor method provided in SDK v and passing in the specified `key` and `value` parameters, you can quickly integrate real-time AI voice conversion ability into your . The `key` parameter you specify in the `setExtensionPropertyWithVendor` method corresponds to the name of the API, and the `value` parameter wraps some or all of the parameters of the API in JSON format. By passing in the specified `key` and `value` parameters, you can call the corresponding API to realize the functions related to real-time AI sound conversion. diff --git a/shared/extensions-marketplace/dubbing-voice-changer/project-implementation/android.mdx b/shared/extensions-marketplace/dubbing-voice-changer/project-implementation/android.mdx index 46fe01fe1..a6de16cb2 100644 --- a/shared/extensions-marketplace/dubbing-voice-changer/project-implementation/android.mdx +++ b/shared/extensions-marketplace/dubbing-voice-changer/project-implementation/android.mdx @@ -4,7 +4,7 @@ To integrate the extension in your project: - 1. Enter the [Agora console > Cloud Market](https://console.agora.io/marketplace/list/all) page and download the **** extension package. After unzipping, save all `.aar` files to the project folder `/app/libs`. + 1. Enter the Extensions Marketplace and download the **** extension package. After unzipping, save all `.aar` files to the project folder `/app/libs`. 1. Get the following resource files and save them in the same directory of the project folder. For example, a new `vc_model` directory: - License file and tone files: Contact to obtain these files. The suffix of the tone file is `.dat`, and the tone file is issued according to the license. - Model file: Download the required resources. diff --git a/shared/extensions-marketplace/dubbing-voice-changer/project-implementation/ios.mdx b/shared/extensions-marketplace/dubbing-voice-changer/project-implementation/ios.mdx index 53a0d4b06..de7395206 100644 --- a/shared/extensions-marketplace/dubbing-voice-changer/project-implementation/ios.mdx +++ b/shared/extensions-marketplace/dubbing-voice-changer/project-implementation/ios.mdx @@ -4,7 +4,7 @@ To integrate the extension in your project: - 1. Enter the [Agora console > Cloud Market](https://console.agora.io/marketplace/list/all) page and download the plug-in package for **** plug-in. + 1. Enter the Extensions Marketplace page and download the plug-in package for **** plug-in. 1. Unzip the folder, import all `.framework` files into the project, and modify `Embed` to `Embed & Sign`. 1. Get the following resource files and save them in the same directory of the project folder (For example, Resource path): - License file and tone files: Contact to obtain these files. The suffix of the tone file is named `.dat`, and the tone file is issued according to the license. diff --git a/shared/extensions-marketplace/dubbing-voice-changer/reference/android.mdx b/shared/extensions-marketplace/dubbing-voice-changer/reference/android.mdx index 78ff0e1c5..d9e949068 100644 --- a/shared/extensions-marketplace/dubbing-voice-changer/reference/android.mdx +++ b/shared/extensions-marketplace/dubbing-voice-changer/reference/android.mdx @@ -14,7 +14,7 @@ The extension provides a [GitHub sample project](https://git ### API reference -- addExtension in the `RtcEngineConfig` class +- addExtension in the `RtcEngineConfig` class - enableExtension in the `RtcEngine` class - setExtensionProperty in the `RtcEngine` class diff --git a/shared/extensions-marketplace/dubbing-voice-changer/reference/ios.mdx b/shared/extensions-marketplace/dubbing-voice-changer/reference/ios.mdx index 3e4cb99fd..745abae6d 100644 --- a/shared/extensions-marketplace/dubbing-voice-changer/reference/ios.mdx +++ b/shared/extensions-marketplace/dubbing-voice-changer/reference/ios.mdx @@ -15,7 +15,7 @@ The extension provides a [GitHub sample project](https://git ### API reference -- enableExtensionWithVendor in the `AgoraRtcEngineKit` class -- setExtensionPropertyWithVendor in the `AgoraRtcEngineKit` class +- enableExtensionWithVendor in the `AgoraRtcEngineKit` class +- setExtensionPropertyWithVendor in the `AgoraRtcEngineKit` class \ No newline at end of file diff --git a/shared/extensions-marketplace/faceunity/project-implementation/ios.mdx b/shared/extensions-marketplace/faceunity/project-implementation/ios.mdx index b12b97576..8294c2cea 100644 --- a/shared/extensions-marketplace/faceunity/project-implementation/ios.mdx +++ b/shared/extensions-marketplace/faceunity/project-implementation/ios.mdx @@ -16,7 +16,7 @@ For details of files provided in the resource pack, see [Resource package structure](#resource-package-structure). 1. Import the required header files. Add the following statements to your code: - ``` objective-c + ```objective-c #import #import "authpack.h" ``` diff --git a/shared/extensions-marketplace/image-enhancement.mdx b/shared/extensions-marketplace/image-enhancement.mdx index c1693ab4e..1dd25e774 100644 --- a/shared/extensions-marketplace/image-enhancement.mdx +++ b/shared/extensions-marketplace/image-enhancement.mdx @@ -53,7 +53,7 @@ To integrate and implement the image enhancement extension, follow these steps: Method two: Use the Script tag in the HTML file. Once imported, the `BeautyExtension` instance can be used directly in JavaScript files. - ``` xml + ```xml ``` @@ -198,16 +198,6 @@ disable(): void | Promise; Disables the image enhancement extension. -#### release - -```typescript -release(): Promise; -``` - -Releases all resources used by the extension, including Web Workers. - -If `IBeautyProcessor` is repeatedly created without releasing the resources occupied by the extension, it may cause memory exhaustion. - #### onoverload ```typescript diff --git a/shared/extensions-marketplace/integrations/bose-pinpoint.mdx b/shared/extensions-marketplace/integrations/bose-pinpoint.mdx deleted file mode 100644 index 29bf40bf9..000000000 --- a/shared/extensions-marketplace/integrations/bose-pinpoint.mdx +++ /dev/null @@ -1,160 +0,0 @@ ---- -title: "Bose PinPoint" -sidebar_position: 2 -type: docs -description: > - Accurately transcribe a conversation and discover insights. ---- -export const toc = [{}]; - -This guide is provided by Bose PinPoint. Agora is planning a documentation upgrade program for all extensions on the marketplace. Please stay tuned. - -The Bose PinPoint extension reduces noise from the local microphone signal. To use our extension you just need to call the standard Agora-provided methods. - - - -This extension is not provided for this platform. - - - - - - - -## Integrate Bose PinPoint extension - -### Download and link to the extension - -First, activate the extension in Agora Console on your project. Activate the extension by clicking **Activate** and agree to the prompt. Enable the extension on the projects you wish to integrate the PinPoint Extension with. Finally click **View**, under **Credentials**, on that project and make note of the API Key and API Secret. - -Add the `Android Archive` file, `agora-pinpoint-release.aar` to your project, and note the directory. In this example the file `agora-pinpoint-release.aar` was added to the `app/libs` directory. Then add the dependency to the app's `build.gradle` by adding the line `implementation(files("libs/agora-pinpoint-release.aar"))` under dependencies - - -On iOS the `.framework` file has to be embedded in your project, on Android set up the `.aar` file as a dependency. - - -### Getting started - -To configure the extension, use the `setExtensionPropertyWithVendor` method. You need to set your API Key and API Secret values using this method. You can find these values for your project in the Agora Extensions Marketplace. - - - -You can also find the example app in the `android/example` directory in the extension package. Go to the section titled Using The Example Projects for more information about running the example projects. - -```java -mRtcEngine.setExtensionProperty("Bose", "PinPoint", "apiKey", "EXAMPLE_API_KEY"); mRtcEngine.setExtensionProperty("Bose", "PinPoint", "apiSecret", "EXAMPLE_API_SECRET"); -``` - - - -You can also find the example app in the `ios/example/` directories in the extension package. Go to the section titled Using The Example Projects for more information about running the example projects. - -```objective-c -[self.agoraKit setExtensionPropertyWithVendor:@"Bose" - extension:@"PinPoint" - key:@"apiKey" - value:@"EXAMPLE_API_KEY"]; -[self.agoraKit setExtensionPropertyWithVendor:@"Bose" - extension:@"PinPoint" - key:@"apiSecret" - value:@"EXAMPLE_API_SECRET"]; -``` - - -### Adding the extension - -To add the extension to your instance of RtcEngine include the following call to addExtension when setting up your RtcEngineConfig - -```java -RtcEngineConfig config = new RtcEngineConfig(); -config.mContext = this; -config.mAppId = AppConfig.appId; -config.addExtension(ExtensionManager.EXTENSION_NAME); -config.mExtensionObserver = this; -config.mEventHandler = new IRtcEngineEventHandler() { -``` - - -### Enabling the extension - - - -```java -PinPoint.configure(getAssets()); -mRtcEngine.enableExtension(“Bose”, ”PinPoint”, true); -``` - - - - -```objective-c -[self.agoraKit enableExtensionWithVendor:@"Bose" - extension:@"PinPoint" - enabled:YES]; -``` - - -After the extension is enabled and the settings are validated, it starts reducing the noise from the local microphone signal automatically - -## Run the demo - -Download Agora Video SDK from Agora Extensions Marketplace and copy the zip file into the `libs/` directory and unzip them. You will need to have at least version 4.0.0 beta to use Extensions. - - - -Open `android/example/app/src/main/java/agoramarketplace/bose/pinpoint/AppConfig.java` - -Set your Agora appID and token (if using one) found in the Agora console - -```java -final static String appId = "YOUR_APP_ID"; -final static String apiKey = "YOUR_API_KEY"; -final static String apiSecret = "YOUR_API_SECRET"; -``` - -Set your Bose PinPoint Extension apiKey and apiSecret found in the Agora Extensions Marketplace. - -If only one device is available to test with, you can join the channel by entering your App ID and channel name, which defaults to agora_extension using one of the Agora Examples. - - - - - -Open `AgoraWithPinPoint.xcodeproj` provided in the `ios/examples` directory. - -Download Agora’s SDKs and copy `Agoraffmpeg.xcframework` and `AgoraRtcKit.xcframework` into the `libs/ directory`. Add the frameworks to your project by navigating to Project settings -> General -> Frameworks, Libraries and Embedded Content in XCode. - -After creating an Agora App in the Agora console, open the file `ios/example/AgoraWithPinPoint/AppID.m`. Set your Agora appID and token (if using one) found in the Agora console - -```objective-c -NSString *const appID = <#Your App Id#>; -NSString *const token = <#Temp Access Token#>; -``` - -Then open `ios/example/AgoraWithPinPoint/VideoChatViewController.m` Set your Bose PinPoint Extension apiKey and apiSecret found in the Agora console. - -## Reference - -### Troubleshooting - - - -If the app crashes immediatly after setting the room name try these troubleshooting options: - -1. Confirm that apiKey, apiSecret, appID and token (if used) are set in the app and match the values present in the Agora Dev Console and Agora . - - - - -If `[self.agoraKit enableExtensionWithVendor:@"Bose" extension:@"PinPoint" enabled:YES]` is returning a negative number. Try these troubleshooting options: - -1. Confirm that apiKey, apiSecret, appID and token (if used) are set in the app and match the values present in the Agora Dev Console and Agora. -2. If it’s still returning a negative number, make sure the `.framework` has been manually added to the Xcode project by adding the Framework to, **Frameworks, Libraries, and Embedded Content**. -3. If while setting up the example app, you receive an error message stating dyld: library not loaded make sure the framework has been marked as, **Embed & Sign**, instead of the default, **Do not Embed**. - - - - - - - \ No newline at end of file diff --git a/shared/extensions-marketplace/livedata-conversation-intelligence/index.mdx b/shared/extensions-marketplace/livedata-conversation-intelligence/index.mdx new file mode 100644 index 000000000..dff29684f --- /dev/null +++ b/shared/extensions-marketplace/livedata-conversation-intelligence/index.mdx @@ -0,0 +1,79 @@ +import ProjectSetup from '@docs/shared/extensions-marketplace/livedata-conversation-intelligence/project-setup/index.mdx'; +import ProjectImplementation from '@docs/shared/extensions-marketplace/livedata-conversation-intelligence/project-implementation/index.mdx'; +import Reference from '@docs/shared/extensions-marketplace/livedata-conversation-intelligence/reference/index.mdx'; + +The extension enables you to quickly add real-time speech transcription and translation features to your . + + +**This extension is not yet available for .** + + + +This page shows you how to integrate and use the extension in your . + +## Understand the tech + + +The extension is an encapsulation of the real-time speech recognition and translation core API on the cloud. You can quickly integrate the capabilities by passing in the specified `key` and `value` parameters to the setExtensionProperty method provided in SDK v. + +The key parameter of the `setExtensionProperty` method corresponds to the name of the API, and the value parameter wraps some or all of the parameters of the API in JSON format. Therefore, by passing in the specified `key` and `value` parameters, you can call the corresponding cloud API to realize the functions of . + + + +The extension is an encapsulation of the real-time speech recognition and translation core API on the cloud. You can quickly integrate the capabilities by passing in the specified `key` and `value` parameters to the setExtensionPropertyWithVendor method provided in SDK v. + +The key parameter of the `setExtensionPropertyWithVendor` method corresponds to the name of the API, and the value parameter wraps some or all of the parameters of the API in JSON format. Therefore, by passing in the specified `key` and `value` parameters, you can call the corresponding cloud API to realize the functions of . + + +## Prerequisites + +Ensure that your development environment meets the following requirements: + + +- Android Studio 4.1 or later. +- A physical device (not an emulator) running Android 5.0 or later. + + + +- Xcode 9.0 or later. +- A physical device (not an emulator) running iOS 9.0 or later. + +- is used with v. + + Refer to the [SDK quickstart](/video-calling/get-started/get-started-sdk) to integrate v and implement basic video calling. + +## Project Setup + +The extension provides a sample project on GitHub to help you get started quickly. + + + +2. **Test translation and transcription features** + + Once you have installed the sample project on your device, follow these steps to test the translation and transcription features: + + 1. Start the . Fill in the **channel name** in the input box, and click **Join**. + 1. Click **Start Translation** to begin the translation. Speak into the device. You see the transcription and translation on the screen in real-time. + 1. Click **End Translation** to end transcription and translation. + 1. Click **End Plug-in** to stop using the extension. + + +## Integrate the extension + +This section shows you how to integrate the extension, and call the core API to perform real-time speech recognition and translation. + +To integrate the extension in your project: + +1. **Purchase and activate the extension** + + Visit the Extensions Marketplace and follow the prompts to purchase the extension. Save the `appKey` and `appSecret` you obtain. You use these values to initialize the extension. + + + +## Reference + +This section contains information that completes the information in this page, or points you to documentation that explains other aspects to this product. + + + + \ No newline at end of file diff --git a/shared/extensions-marketplace/livedata-conversation-intelligence/project-implementation/android.mdx b/shared/extensions-marketplace/livedata-conversation-intelligence/project-implementation/android.mdx new file mode 100644 index 000000000..fae47419c --- /dev/null +++ b/shared/extensions-marketplace/livedata-conversation-intelligence/project-implementation/android.mdx @@ -0,0 +1,61 @@ + + +2. **Integrate the extension** + + Refer to the following steps: + + 1. From the Extensions Marketplace, download the extension package of the extension. Unzip the package and save all `.aar` files to the `/app/libs` path in your project folder. + + 1. Open `app/build.gradle` and add the following line under `dependencies`: + + ```java + implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"]) + ``` + +1. **Enable the extension** + + When initializing the , call `addExtension` to load the extension and then call `enableExtension` to enable it: + + ```java + RtcEngineConfig config = new RtcEngineConfig(); + config.addExtension("agora-iLiveData-filter"); + engine = RtcEngine.create(config); + engine.enableExtension("iLiveData", "RTVT", true); + ``` + +1. **Start transcription and translation** + + Prepare a JSON object to pass in values for the `appKey` and `appSecret` parameters: + + ```java + JSONObject jsonObject = new JSONObject(); + // Pass in the `appKey` and `appSecret` obtained when purchasing and activating the extension in the Agora console. + jsonObject.put("appKey", "80001000"); + jsonObject.put("appSecret", "qwerty"); + // Set source language + jsonObject.put("srclang", "zh"); + // Set target language + jsonObject.put("dstLang", "en"); + ``` + + Call `setExtensionProperty` with a `startAudioTranslation` key + + ```java + engine.setExtensionProperty(EXTENSION_VENDOR_NAME, + EXTENSION_AUDIO_FILTER_VOLUME, "startAudioTranslation", jsonObject.toString()); + ``` + +1. **Get transcription and translation results** + + After successful initialization, the extension returns the transcription and translation results using the `onEvent` callback. + +1. **Stop using the extension** + + Call the `setExtensionProperty` method and specify the key as `closeAudioTranslation` to end the use of the extension: + + ```java + engine.setExtensionProperty(EXTENSION_VENDOR_NAME, + EXTENSION_AUDIO_FILTER_VOLUME, "closeAudioTranslation", "end"); + ``` + + \ No newline at end of file diff --git a/shared/interactive-whiteboard/fastboard-api/index.mdx b/shared/extensions-marketplace/livedata-conversation-intelligence/project-implementation/index.mdx similarity index 69% rename from shared/interactive-whiteboard/fastboard-api/index.mdx rename to shared/extensions-marketplace/livedata-conversation-intelligence/project-implementation/index.mdx index 24fa6e5c5..9a85741ba 100644 --- a/shared/interactive-whiteboard/fastboard-api/index.mdx +++ b/shared/extensions-marketplace/livedata-conversation-intelligence/project-implementation/index.mdx @@ -1,8 +1,5 @@ import Android from './android.mdx'; import Ios from './ios.mdx'; -import Web from './web.mdx'; - - diff --git a/shared/extensions-marketplace/livedata-conversation-intelligence/project-implementation/ios.mdx b/shared/extensions-marketplace/livedata-conversation-intelligence/project-implementation/ios.mdx new file mode 100644 index 000000000..ee1844d27 --- /dev/null +++ b/shared/extensions-marketplace/livedata-conversation-intelligence/project-implementation/ios.mdx @@ -0,0 +1,82 @@ + + +2. **Integrate the extension** + + Refer to the following steps: + + 1. From the Extensions Marketplace, download the extension package of the extension. After unzipping, save all `.framework` library files to your project folder. Using the following project structure as an example, save the library file under the `` path: + + ```sh + . + ├── + ├── .xcodeproj + ``` + + 1. In Xcode [add the dynamic library](https://help.apple.com/xcode/mac/current/#/dev51a648b07), make sure the **Embed** property is set to **Embed & Sign**. + + 1. Select **TARGETS**, click **Build Settings**, select **All** view, click **Other Linker Flags**, and add `-ObjC`: + ![Add ObjC](/images/extensions-marketplace/livedata-ios-build-settings-add-objc.png) + Note that the letters `O` and `C` need to be capitalized and the symbol `-` should not be ignored: + ![Capitalize O and C](/images/extensions-marketplace/livedata-ios-build-settings-capitalize-o-and-c.png) + + 1. Make sure that the project has at least one `.mm` file: The plug-in library files are implemented in Objective-C++, so you need to have at least one source file with a `.mm` suffix in your project. You can use any `.m` file with the suffix modified to `.mm`. + +1. **Enable the extension** + + When initializing the , call `enableExtensionWithVendor` to enable the extension: + + ```objective-c + AgoraRtcEngineConfig *config = [AgoraRtcEngineConfig new]; + + // Listen to the extension events + config.eventDelegate = self; + self.agoraKit = [AgoraRtcEngineKit sharedEngineWithConfig:config + delegate:self]; + // Enable the extension + [self.kit enableExtensionWithVendor:[iLiveDataSimpleFilterManager companyName] + extension:[iLiveDataSimpleFilterManager plugName] + enabled:YES]; + ``` + +1. **Start transcription and translation** + + Call `setExtensionPropertyWithVendor` with a `startAudioTranslation` key and pass in values for `appKey`, `appSecret` parameters in JSON format: + + ```objective-c + NSDictionary * startDic = @{ + // Pass in the `appKey` obtained after purchasing and activating the extension in the Agora console. + @"appKey":, + // Pass in the `appSecret` obtained after purchasing and activating the extension in the Agora console. + @"appSecret":, + // Set source language + @"srcLanguage":@"zh", + // Set target language + @"destLanguage":@"en" + }; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:startDic options:NSJSONWritingPrettyPrinted error:nil]; + NSString * jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + ``` + + ```objective-c + [self.kit setExtensionPropertyWithVendor:[iLiveDataSimpleFilterManager companyName] + extension:[iLiveDataSimpleFilterManager plugName] + key:"startAudioTranslation" + value:jsonStr]; + ``` + +1. **Get transcription and translation results** + + After successful initialization, the cloud extension returns the rewriting and translation results using the `onEvent` callback. + +1. **Stop using the extension** + + Call the `setExtensionPropertyWithVendor` method and specify the key as `closeAudioTranslation` to end the use of the extension: + + ```objective-c + [self.kit setExtensionPropertyWithVendor:[iLiveDataSimpleFilterManager companyName] + extension:[iLiveDataSimpleFilterManager plugName] + key:"closeAudioTranslation" + value:"end"]; + ``` + + \ No newline at end of file diff --git a/shared/extensions-marketplace/livedata-conversation-intelligence/project-setup/android.mdx b/shared/extensions-marketplace/livedata-conversation-intelligence/project-setup/android.mdx new file mode 100644 index 000000000..103d24815 --- /dev/null +++ b/shared/extensions-marketplace/livedata-conversation-intelligence/project-setup/android.mdx @@ -0,0 +1,22 @@ + + +1. **Setup the sample project** + + To set up and run the sample project, do the following: + + 1. Clone the [Github repository](https://github.com/highras/rtvt-agora-marketplace). + + Execute the following command in the terminal: + + ```shell + git clone https://github.com/highras/rtvt-agora-marketplace.git + ``` + + 1. Open the sample project in Android Studio. + + 1. Sync project with Gradle files. + + 1. Connect a real Android device (not a simulator) and run the project. + + + \ No newline at end of file diff --git a/shared/extensions-marketplace/livedata-conversation-intelligence/project-setup/index.mdx b/shared/extensions-marketplace/livedata-conversation-intelligence/project-setup/index.mdx new file mode 100644 index 000000000..9a85741ba --- /dev/null +++ b/shared/extensions-marketplace/livedata-conversation-intelligence/project-setup/index.mdx @@ -0,0 +1,5 @@ +import Android from './android.mdx'; +import Ios from './ios.mdx'; + + + diff --git a/shared/extensions-marketplace/livedata-conversation-intelligence/project-setup/ios.mdx b/shared/extensions-marketplace/livedata-conversation-intelligence/project-setup/ios.mdx new file mode 100644 index 000000000..f3c2fe11f --- /dev/null +++ b/shared/extensions-marketplace/livedata-conversation-intelligence/project-setup/ios.mdx @@ -0,0 +1,26 @@ + + +1. **Setup the sample project** + + To set up and run the sample project, do the following: + + 1. Clone the [Github repository](https://github.com/highras/rtvt-agora-marketplace). + + Execute the following command in the terminal: + + ```shell + git clone https://github.com/highras/rtvt-agora-marketplace.git + ``` + + 1. Enter the directory `iOS/SW_Test` in the terminal and run the following command to install dependencies using CocoaPods: + + ```shell + pod install + ``` + + 1. Open **SW_Test.xcworkspace** from Xcode to open the sample project. + + 1. Connect a real iOS device (not a simulator) and run the project. + + + \ No newline at end of file diff --git a/shared/extensions-marketplace/livedata-conversation-intelligence/reference/android.mdx b/shared/extensions-marketplace/livedata-conversation-intelligence/reference/android.mdx new file mode 100644 index 000000000..a275d4b64 --- /dev/null +++ b/shared/extensions-marketplace/livedata-conversation-intelligence/reference/android.mdx @@ -0,0 +1,9 @@ + + +### API reference + +- addExtension in the `RtcEngineConfig` class +- enableExtension in the `RtcEngine` class +- setExtensionProperty in the `RtcEngine` class + + \ No newline at end of file diff --git a/shared/extensions-marketplace/livedata-conversation-intelligence/reference/index.mdx b/shared/extensions-marketplace/livedata-conversation-intelligence/reference/index.mdx new file mode 100644 index 000000000..9a85741ba --- /dev/null +++ b/shared/extensions-marketplace/livedata-conversation-intelligence/reference/index.mdx @@ -0,0 +1,5 @@ +import Android from './android.mdx'; +import Ios from './ios.mdx'; + + + diff --git a/shared/extensions-marketplace/livedata-conversation-intelligence/reference/ios.mdx b/shared/extensions-marketplace/livedata-conversation-intelligence/reference/ios.mdx new file mode 100644 index 000000000..315b18515 --- /dev/null +++ b/shared/extensions-marketplace/livedata-conversation-intelligence/reference/ios.mdx @@ -0,0 +1,8 @@ + + +### API reference + +- enableExtensionWithVendor in the `AgoraRtcEngineKit` class +- setExtensionPropertyWithVendor in the `AgoraRtcEngineKit` class + + \ No newline at end of file diff --git a/shared/extensions-marketplace/reference/_ains.mdx b/shared/extensions-marketplace/reference/_ains.mdx index d0fa3a044..f29c182ce 100644 --- a/shared/extensions-marketplace/reference/_ains.mdx +++ b/shared/extensions-marketplace/reference/_ains.mdx @@ -1,4 +1,4 @@ -**Agora charges additionally for this extension. See [Pricing](/video-calling/reference/pricing#ai-noise-suppression-pricing).** +**Agora charges additionally for this extension. See [Pricing](/video-calling/overview/pricing#ai-noise-suppression-pricing).** ### v1.1.0 diff --git a/shared/extensions-marketplace/use-an-extension/project-implementation/android.mdx b/shared/extensions-marketplace/use-an-extension/project-implementation/android.mdx index debd89227..1da4a40dc 100644 --- a/shared/extensions-marketplace/use-an-extension/project-implementation/android.mdx +++ b/shared/extensions-marketplace/use-an-extension/project-implementation/android.mdx @@ -11,14 +11,14 @@ in your Agora project: 1. Add the following lines to import the Android classes used by the extension: - ``` java + ```java import org.json.JSONException; import org.json.JSONObject; ``` 2. Add the following lines to import the Agora classes used by the extension: - ``` java + ```java // ExtensionManager is used to pass in basic info about the extension import io.agora.extension.ExtensionManager; import io.agora.rtc2.IMediaExtensionObserver; diff --git a/shared/extensions-marketplace/use-an-extension/project-implementation/electron.mdx b/shared/extensions-marketplace/use-an-extension/project-implementation/electron.mdx index d54f99a8b..bfaace777 100644 --- a/shared/extensions-marketplace/use-an-extension/project-implementation/electron.mdx +++ b/shared/extensions-marketplace/use-an-extension/project-implementation/electron.mdx @@ -17,7 +17,7 @@ This section shows you how to implement the video filter extension in your , call `loadExtensionProvider` and pass the extension path. To enable the extension, call `enableExtension` and pass the provider name and extension name. To implement this workflow, in `preload.js`, add the following method before `window.onload = () =>`: - ``` javascript + ```javascript function enableExtension() { if (!path) { diff --git a/shared/extensions-marketplace/use-an-extension/project-implementation/flutter.mdx b/shared/extensions-marketplace/use-an-extension/project-implementation/flutter.mdx index acb67aef8..c0eabe063 100644 --- a/shared/extensions-marketplace/use-an-extension/project-implementation/flutter.mdx +++ b/shared/extensions-marketplace/use-an-extension/project-implementation/flutter.mdx @@ -6,7 +6,7 @@ This section presents the framework code you add to your projec You call `loadExtensionProvider` during initialization to specify the extension library path. To do this, add the following code to the `setupVideoSDKEngine` method after you initialize the engine with `agoraEngine.initialize`: - ``` dart + ```dart agoraEngine.loadExtensionProvider(""); ```` @@ -14,7 +14,7 @@ This section presents the framework code you add to your projec To enable the extension, add the following code to the `setupVideoSDKEngine` method before `await agoraEngine.enableVideo();`: - ``` dart + ```dart // Extensions marketplace hosts both third-party extensions as well as those developed by Agora. To use an Agora extensions, you do not need to call addExtension or enableExtension. agoraEngine.enableExtension( provider: "", @@ -31,7 +31,7 @@ This section presents the framework code you add to your projec To customize the extension for your particular , set suitable values for the extension properties. Refer to the extension documentation for a list of available property names and allowable values. To set a property, add the following code to the `setupVideoSDKEngine` method after `agoraEngine.enableExtension`: - ``` dart + ```dart agoraEngine.setExtensionProperty( provider: "", extension: "", diff --git a/shared/extensions-marketplace/use-an-extension/project-implementation/react-native.mdx b/shared/extensions-marketplace/use-an-extension/project-implementation/react-native.mdx index 93a8dd27e..30e023abf 100644 --- a/shared/extensions-marketplace/use-an-extension/project-implementation/react-native.mdx +++ b/shared/extensions-marketplace/use-an-extension/project-implementation/react-native.mdx @@ -78,7 +78,7 @@ You can use a `switch` to enable and disable the extension you wish to integrate To get notified of important events, add the following callbacks to `agoraEngine.registerEventHandler({`: - ``` ts + ```ts onExtensionErrored: ( provider: string, extName: string, diff --git a/shared/extensions-marketplace/use-an-extension/project-implementation/swift.mdx b/shared/extensions-marketplace/use-an-extension/project-implementation/swift.mdx index 3fae0b6d2..d3c08cebb 100644 --- a/shared/extensions-marketplace/use-an-extension/project-implementation/swift.mdx +++ b/shared/extensions-marketplace/use-an-extension/project-implementation/swift.mdx @@ -3,5 +3,5 @@ To use an extension in your project: - The implementation procedure varies according to extensions. Each extension vendor provides their own implementation guides, which is validated by Agora before the official release of the extension. -- To implement the extension in your project, go to the detail page of the extension on [Agora Console](https://console.agora.io/marketplace/extension), click **Implementation guides**, and follow the steps on the page. +- To implement the extension in your project, go to the detail page of the extension on , click **Implementation guides**, and follow the steps on the page. diff --git a/shared/extensions-marketplace/use-an-extension/project-implementation/unity.mdx b/shared/extensions-marketplace/use-an-extension/project-implementation/unity.mdx index 03c5b72b4..a0f44998e 100644 --- a/shared/extensions-marketplace/use-an-extension/project-implementation/unity.mdx +++ b/shared/extensions-marketplace/use-an-extension/project-implementation/unity.mdx @@ -6,7 +6,7 @@ This section presents the framework code you add to your projec You call `loadExtensionProvider` during initialization to specify the extension library path. To do this, add the following code to the `SetupVideoSDKEngine` method after you initialize the engine with `RtcEngine.Initialize`: - ``` csharp + ```csharp RtcEngine.LoadExtensionProvider(""); ``` @@ -14,7 +14,7 @@ This section presents the framework code you add to your projec To enable the extension, add the following code to the `Join` method before `RtcEngine.EnableVideo();`: - ``` csharp + ```csharp RtcEngine.EnableExtension( provider: "", extension: "", @@ -31,7 +31,7 @@ This section presents the framework code you add to your projec To customize the extension for your particular , set suitable values for the extension properties. Refer to the extension documentation for a list of available property names and allowable values. To set a property, add the following code to the `Join` method after `RtcEngine.EnableExtension`: - ``` csharp + ```csharp RtcEngine.SetExtensionProperty( provider: "", extension: "", diff --git a/shared/extensions-marketplace/use-an-extension/project-implementation/web.mdx b/shared/extensions-marketplace/use-an-extension/project-implementation/web.mdx index 1cf35f79e..6de05673a 100644 --- a/shared/extensions-marketplace/use-an-extension/project-implementation/web.mdx +++ b/shared/extensions-marketplace/use-an-extension/project-implementation/web.mdx @@ -4,7 +4,7 @@ To use an extension in your project: - The implementation procedure varies according to extensions. Each extension vendor provides their own implementation guides, which is validated by Agora before the official release of the extension. -- To implement the extension in your project, go to the detail page of the extension on [Agora Console](https://console.agora.io/marketplace/extension), click **Implementation guides**, and follow the steps on the page. +- To implement the extension in your project, go to the detail page of the extension on Extensions Marketplace, click **Implementation guides**, and follow the steps on the page. Currently, Web extension is in Beta. To learn how to use a Web extension, see [](/video-calling/develop/ai-noise-suppression). diff --git a/shared/extensions-marketplace/use-an-extension/project-setup/android.mdx b/shared/extensions-marketplace/use-an-extension/project-setup/android.mdx index e62208cd3..57036238c 100644 --- a/shared/extensions-marketplace/use-an-extension/project-setup/android.mdx +++ b/shared/extensions-marketplace/use-an-extension/project-setup/android.mdx @@ -11,7 +11,7 @@ 2. In `/Gradle Scripts/build.gradle(Module: app)`, add the following line under `dependencies`: - ``` java + ```java implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs') ``` diff --git a/shared/extensions-marketplace/use-an-extension/project-setup/web.mdx b/shared/extensions-marketplace/use-an-extension/project-setup/web.mdx index 1fb4d8f21..2b7c2a73b 100644 --- a/shared/extensions-marketplace/use-an-extension/project-setup/web.mdx +++ b/shared/extensions-marketplace/use-an-extension/project-setup/web.mdx @@ -9,7 +9,7 @@ 1. In the root of your project, install the extension package. For example, for : - ``` bash + ```bash npm i agora-extension-ai-denoiser ``` diff --git a/shared/extensions-marketplace/virtual-background.mdx b/shared/extensions-marketplace/virtual-background.mdx index 4aadae285..e8a0c5b38 100644 --- a/shared/extensions-marketplace/virtual-background.mdx +++ b/shared/extensions-marketplace/virtual-background.mdx @@ -1,7 +1,7 @@ import ProjectSetup from '@docs/shared/extensions-marketplace/virtual-background/project-setup/index.mdx'; import ProjectImplement from '@docs/shared/extensions-marketplace/virtual-background/project-implementation/index.mdx'; import ProjectTest from '@docs/shared/extensions-marketplace/virtual-background/project-test/index.mdx'; -import Reference from '@docs/shared/extensions-marketplace/virtual-background/reference/index.mdx'; +import Reference from '@docs/shared/extensions-marketplace/virtual-background/reference/index.mdx'; Virtual Background enables users to blur their background or replace it with a solid color or an image. This feature is applicable to scenarios such as online conferences, online classes, and live streaming. It helps protect personal privacy and reduces audience distraction. @@ -27,10 +27,6 @@ Virtual Background enables users to blur their background or replace it with a s Want to test ? Try the online demo. - - **Coming soon for this beta program.** - - ## Understand the tech @@ -46,7 +42,7 @@ A typical transmission pipeline in the Agora Web SDK consists of a chain of proc -## Test your implementation +## Test virtual background @@ -56,5 +52,3 @@ This section contains information that completes the information in this page, o - - diff --git a/shared/extensions-marketplace/virtual-background/project-implementation/index.mdx b/shared/extensions-marketplace/virtual-background/project-implementation/index.mdx index 1baaafc47..fe3f01309 100644 --- a/shared/extensions-marketplace/virtual-background/project-implementation/index.mdx +++ b/shared/extensions-marketplace/virtual-background/project-implementation/index.mdx @@ -1,22 +1,14 @@ -import Android from './android.mdx'; -import Web from './web.mdx'; import Windows from './windows.mdx'; -import Ios from './ios.mdx'; -import MacOs from './macos.mdx'; +import Poc3 from './poc3.mdx'; import Electron from './electron.mdx'; import Flutter from './flutter.mdx'; import ReactNative from './react-native.mdx'; -import Unity from './unity.mdx'; import Unreal from './unreal.mdx'; + - - - - - - \ No newline at end of file + diff --git a/shared/extensions-marketplace/virtual-background/project-implementation/poc3.mdx b/shared/extensions-marketplace/virtual-background/project-implementation/poc3.mdx new file mode 100644 index 000000000..ba932a791 --- /dev/null +++ b/shared/extensions-marketplace/virtual-background/project-implementation/poc3.mdx @@ -0,0 +1,77 @@ +import ImportLibrary from '@docs/assets/code/video-sdk/virtual-background/import-library.mdx'; +import DeviceCompatibility from '@docs/assets/code/video-sdk/virtual-background/device-compatibility.mdx'; +import BlurBackground from '@docs/assets/code/video-sdk/virtual-background/blur-background.mdx'; +import ColorBackground from '@docs/assets/code/video-sdk/virtual-background/color-background.mdx'; +import ImageBackground from '@docs/assets/code/video-sdk/virtual-background/image-background.mdx'; +import ResetBackground from '@docs/assets/code/video-sdk/virtual-background/reset-background.mdx'; +import SetVirtualBackground from '@docs/assets/code/video-sdk/virtual-background/set-virtual-background.mdx'; +import ConfigureEngine from '@docs/assets/code/video-sdk/virtual-background/configure-engine.mdx'; + + + + +### Add the required imports + + + + +### Configure the + + + +### Check device compatibility + + To avoid performance degradation or unavailable features when enabling virtual background on low-end devices, check whether the device supports the feature. + + + +### Configure the virtual background extension + + +### Set a blurred background + + + +### Set a color background + + + +### Set an image background + + + +### Reset the background + + + + +### Check device compatibility + + Not all devices have the capability of using Agora's background segmentation. + Check whether the device supports the specified advanced feature. + + + +### Set a blurred background + + + +### Set a color background + + + +### Set an image background + + + +### Reset the background + + + + + + +### Enable virtual background + + + diff --git a/shared/extensions-marketplace/virtual-background/project-implementation/swift.mdx b/shared/extensions-marketplace/virtual-background/project-implementation/swift.mdx index 2b63e193d..1cf9dc819 100644 --- a/shared/extensions-marketplace/virtual-background/project-implementation/swift.mdx +++ b/shared/extensions-marketplace/virtual-background/project-implementation/swift.mdx @@ -26,7 +26,7 @@ To enable and change virtual backgrounds, you add a button to the user interface Paste the following lines inside the `initViews` function: - ``` swift + ```swift // Button to change virtual background BackgroundButton = NSButton() BackgroundButton.frame = CGRect(x: 230, y:240, width:80, height:20) @@ -37,7 +37,7 @@ To enable and change virtual backgrounds, you add a button to the user interface ``` - ``` swift + ```swift // Button to change virtual background BackgroundButton = UIButton(type: .system) BackgroundButton.frame = CGRect(x: 60, y:500, width:250, height:50) diff --git a/shared/extensions-marketplace/virtual-background/project-implementation/unity.mdx b/shared/extensions-marketplace/virtual-background/project-implementation/unity.mdx index 4a3b6ce4e..46fdf9e2d 100644 --- a/shared/extensions-marketplace/virtual-background/project-implementation/unity.mdx +++ b/shared/extensions-marketplace/virtual-background/project-implementation/unity.mdx @@ -1,94 +1,36 @@ - -This section explains how to enable your users to choose a virtual background. - -### Implement the user interface - -To enable and change virtual backgrounds, you add a button to the user interface. To implement this user interface, take the following steps: - - 1. Right-click **Sample Scene**, then click **Game Object** > **UI** > **Button - TextMeshPro**. A button appears in the **Scene** Canvas. - - 2. In **Inspector**, rename **Button** to **virtualBackground**. - - 3. In **SampleScene**, click **Canvas** > **virtualBackground**, and then in **Inspector**, change the following coordinates: - - * **Pos X** - 350 - * **Pos Y** - 172 - -### Set a virtual background - -1. **Define variables to keep track of the virtual background state** - - In your script file, add the following declarations to `NewBehaviourScript`: - - ```csharp - int counter = 0; // to cycle through the different types of backgrounds - bool isVirtualBackGroundEnabled = false; - ``` - -2. **Enable virtual background** - - When a user presses the button, you check if the user's device supports the virtual background feature. If `IsFeatureAvailableOnDevice` returns true, you call `EnableVirtualBackground` to enable background blur. When the user presses the button again, you change the virtual background to a solid color. On the next button press, you set a `.jpg` or `.png` image as the virtual background. To specify these background effects, you configure `VirtualBackgroundSource` and `SegmentationProperty`. To implement this workflow, in your script file, add the following method to the `NewBehaviourScript` class: + When a user presses the button, you call `SetVirtualBackground` to enable background blur. When the user presses the button again, you change the virtual background to a solid color. On the next button press, you set a `.jpg` or `.png` image as the virtual background. To specify these background effects, you configure `VirtualBackgroundSource` and `SegmentationProperty`. ```csharp - public void setVirtualBackground() + public void SetVirtualBackground() { - if(!RtcEngine.IsFeatureAvailableOnDevice(FeatureType.VIDEO_VIRTUAL_BACKGROUND)) - { - Debug.Log("Your device does not support virtual background"); - return; - } + TMP_Text BtnText = virtualBackgroundGo.GetComponentInChildren(true); + // Options for virtual background + string[] options = { "Color", "Blur", "Image" }; - counter++; - if (counter > 3) + if (counter >= 3) { - counter = 0; isVirtualBackGroundEnabled = false; + BtnText.text = "Enable Virtual Background"; + counter = 0; Debug.Log("Virtual background turned off"); } else { isVirtualBackGroundEnabled = true; + BtnText.text = "Background :" + options[counter]; } - VirtualBackgroundSource virtualBackgroundSource = new VirtualBackgroundSource(); - // Set the type of virtual background - if (counter == 1) - { // Set background blur - virtualBackgroundSource.background_source_type = BACKGROUND_SOURCE_TYPE.BACKGROUND_BLUR; - virtualBackgroundSource.blur_degree = BACKGROUND_BLUR_DEGREE.BLUR_DEGREE_HIGH; - Debug.Log("Blur background enabled"); - } - else if (counter == 2) - { // Set a solid background color - virtualBackgroundSource.background_source_type = BACKGROUND_SOURCE_TYPE.BACKGROUND_COLOR; - virtualBackgroundSource.color = 0x0000FF; - Debug.Log("Color background enabled"); - } - else if (counter == 3) - { // Set a background image - virtualBackgroundSource.background_source_type = BACKGROUND_SOURCE_TYPE.BACKGROUND_IMG; - virtualBackgroundSource.source = ""; - Debug.Log("Image background enabled"); - } - - // Set processing properties for background - SegmentationProperty segmentationProperty = new SegmentationProperty(); - segmentationProperty.modelType = SEG_MODEL_TYPE.SEG_MODEL_AI; // Use SEG_MODEL_GREEN if you have a green background - segmentationProperty.greenCapacity = 0.5F; // Accuracy for identifying green colors (range 0-1) - - // Enable or disable virtual background - RtcEngine.EnableVirtualBackground( - isVirtualBackGroundEnabled, - virtualBackgroundSource, segmentationProperty); + // Set the virtual background + virtualBackgroundManager.setVirtualBackground(isVirtualBackGroundEnabled, options[counter]); + counter++; } ``` + For more details, see the following: + + - enableVirtualBackground -3. **Setup an event listener for the virtual background button** + - VirtualBackgroundSource - Call `setVirtualBackground` when the user presses **Virtual Background**. In your script file, add the followig at the end of `SetupUI`: + - SegmentationProperty - ```csharp - go = GameObject.Find("virtualBackground"); - go.GetComponent`: + + ```html + + + ``` + +### Handle the system logic + +To integrate HTTP and JSON capabilities into your for real-time transcription: + +1. **Import the `https` module into your project** + + To call `HTTP` methods that start, stop, and query a real-time transcription task, you add a `https` module to your project. Open `preload.js` and add the following before `let agoraEngine;`: + + ```javascript + var https = require('https'); + ``` + +1. **Define variables to manage a real-time transcription task** + + In `preload.js`, add the following to declarations: + + ```javascript + var baseUrl = "api.agora.io"; + var apiKey = ""; + var apiSecret = ""; + var instanceID = "Test-1"; + var authorizationHeader, builderToken; + var realTimeTranscriptionTaskId = ""; // Returned by the start method + var isRealTimeTranscriptionRunning = false; + ``` + +### Manage a real-time transcription task + +Take the following steps to start, stop, and query a real-time transcription task in your . + +1. **Get a `builderToken`** + + To acquire a `builderToken`, you: + + 1. Generate a Base64-encoded credential from the API key and secret associated with your project. + 1. Add this credential to the request header as a parameter named `Authorization`. + 1. In the request body, pass the `instanceID` that you use to identify the instance. + + To acquire a `builderToken`, in `preload.js`, add the following before `document.getElementById("join").onclick = async function ()`: + + ```javascript + document.getElementById("realTimeTranscription").onclick = async function () + { + if(isRealTimeTranscriptionRunning) + { + stopRealTimeTranscription(); + return; + } + var path = "/v1/projects/" + appID + "/rtsc/speech-to-text/builderTokens"; + var plainCredentials = apiKey + ":" + apiSecret; + var base64Credentials = Buffer.from(plainCredentials).toString('base64') + authorizationHeader = "Basic " + base64Credentials; + var postData = JSON.stringify( + { + "instanceId": instanceID + }); + // Set request parameters + const options = + { + hostname: baseUrl, + port: 443, + path: path, + method: 'POST', + headers: + { + 'Authorization':authorizationHeader, + 'Content-Type': 'application/json', + 'Content-Length': postData.length + } + } + // Create a request object and send request. + const req = https.request(options, res => + { + console.log(`Status code: ${res.statusCode}`) + res.on('data', function(data) + { + // Parse the response to extract the token. + var data = JSON.parse(data); + builderToken = data.tokenName; + startRealTimeTranscription(); + console.log(builderToken); + }) + }) + req.on('error', error => + { + // Display the error result when the request fails. + console.error(error) + }) + // Send the post request data to the upstream server. + req.write(postData); + // End the request. + req.end() + } + ``` + + A `builderToken` is usable for 5 minutes. After the time has expired, you must generate a new token. + +1. **Start a real-time transcription task** + + To start a real-time transcription task, you: + 1. Create a JSON configuration and pass it in the body of a `POST` request. + 1. Add the `builderToken` as a query parameter to the URL. + 1. Make a `POST` call to . + On success, the method returns a `taskId` that you use to query or stop the task. + + To start a task, in `preload.js`, add the following method before `window.onload = () =>`: + + ```javascript + function startRealTimeTranscription() + { + var url = "/v1/projects/" + appID + "/rtsc/speech-to-text/tasks?" + "builderToken=" + builderToken; + // Set up a JSON string to specify the real-time transcription configuration. + var body = JSON.stringify( + { + "audio": + { + "subscribeSource": "AGORARTC", + "agoraRtcConfig": + { + "channelName": channel, + "uid": "1", + "token": token, + "channelType": "LIVE_TYPE", + "subscribeConfig": + { + "subscribeMode": "CHANNEL_MODE" + }, + "maxIdleTime": 60 + } + }, + "config": + { + "features": + [ + "RECOGNIZE" + ], + "recognizeConfig": + { + "language": "ENG", + "model": "Model", + "output": + { + "destinations": + [ + "AgoraRTCDataStream", + "Storage" + ], + "agoraRTCDataStream": + { + "channelName": channel, + "uid": "1", + "token": token + }, + "cloudStorage": + [ + { + "format": "HLS", + "storageConfig": + { + "vendor": 0, + "region": 0, + "bucket":"", + "accessKey":"", + "secretKey":"" + }, + "fileNamePrefix": + [ + "directory1", + "directory2" + ] + } + ] + } + } + } + }); + const options = + { + hostname: baseUrl, + path: url, + method: 'POST', + headers: + { + 'Authorization':authorizationHeader, + 'Content-Type': 'application/json', + 'Content-Length': body.length + } + } + // Create request object and send request + const req = https.request(options, res => + { + console.log(`Status code: ${res.statusCode}`) + res.on('data', (d) => + { + // Parse the response to learn about the status of the real-time transcription task. + var data = JSON.parse(d); + var status = data.status; + realTimeTranscriptionTaskId = data.taskId; + if (status == "STARTED" || status == "IN_PROGRESS") + { + isRealTimeTranscriptionRunning = true; + taskId = data.taskId; + document.getElementById("realTimeTranscription").innerHTML = "Stop real-time transcription"; + console.log("real-time transcription task started"); + } + else + { + isRealTimeTranscriptionRunning = false; + console.log("real-time transcription status: " + status); + } + process.stdout.write(d); + }) + }) + req.on('error', error => + { + // Display the error result when the request fails. + console.error("Error parsing query call response: " + error) + }) + // Send the post request data to the upstream server. + req.write(body); + // End the request. + req.end() + } + ``` + +1. **Query task status** + + To notify the user of any change to the real-time transcription task, you query the task status using a `GET` request containing the `taskId` and `builderToken`. + + In `preload.js`, add the following method before `document.getElementById("join").onclick = async function ()`: + + ```javascript + document.getElementById("realTimeTranscriptionQuery").onclick = async function () + { + var path = "/v1/projects/" + appID + "/rtsc/speech-to-text/tasks/" + realTimeTranscriptionTaskId + "?builderToken=" + builderToken; + // Set request parameters + const options = + { + hostname: baseUrl, + port: 443, + path: path, + method: 'GET', + headers: + { + 'Authorization':authorizationHeader, + 'Content-Type': 'application/json', + } + } + // Create a request object and send request + const req = https.request(options, res => + { + console.log(`Status code: ${res.statusCode}`) + res.on('data', function(data) + { + // Parse the response to extract the query results. + var data = JSON.parse(data); + var status = data.status; + realTimeTranscriptionTaskId = data.taskId; + if (status == "IN_PROGRESS") + { + isRealTimeTranscriptionRunning = true; + document.getElementById("realTimeTranscription").innerHTML = "Stop real-time transcription"; + console.log("real-time transcription is running"); + } + else + { + isRealTimeTranscriptionRunning = false; + realTimeTranscriptionTaskId = ""; + console.log("real-time transcription status: " + status + ""); + document.getElementById("realTimeTranscription").innerHTML = "Start real-time transcription"; + } + }) + }) + req.on('error', error => + { + // Display the error result when the request fails. + console.error(error) + }) + // End the request. + req.end() + } + ``` + +1. **Stop the task** + + To stop real-time transcription, you send a `DELETE` request with the `appId`, `taskId`, and `builderToken` in the URL. + + In the `preload.js`, add the following method before `window.onload = () =>`: + + ```javascript + function stopRealTimeTranscription() + { + var path = "/v1/projects/" + appID + + "/rtsc/speech-to-text/tasks/" + realTimeTranscriptionTaskId + + "?builderToken=" + builderToken; + const options = + { + hostname: baseUrl, + port: 443, + path: path, + method: 'DELETE', + headers: + { + 'Authorization': authorizationHeader, + 'Content-Type': 'application/json', + } + } + // Create a request object and send request + const req = https.request(options, res => + { + console.log(`Status code: ${res.statusCode}`) + if(res.statusCode == "200") + { + console.log("Real-time transcription task stopped"); + realTimeTranscriptionTaskId = ""; + document.getElementById("realTimeTranscription").innerHTML = "Start real-time transcription" + isRealTimeTranscriptionRunning = false; + } + res.on('data', (d) => + { + process.stdout.write(d); + }) + }) + req.on('error', error => + { + // Display the error result when the request fails. + console.error("Error while stopping the real-time transcription task: " + error) + }) + // End the request. + req.end() + } + ``` + +1. **Receive and display the text stream** + + When you set one of the `destinations` in the starting configuration as `AgoraRTCDataStream`, the real-time transcription conversion output stream is pushed to your channel and you receive it using `onStreamMessage`. + + To receive this stream and display the text in your , in `preload.js`, add the following code after `const EventHandles = {`: + + ```javascript + onStreamMessage: (connection, remoteUid, streamId, data, length, sentTs) => + { + var text = new TextDecoder().decode(data); + console.log(text); + }, + ``` + diff --git a/shared/video-sdk/develop/real-time-transcription/project-implementation/flutter.mdx b/shared/video-sdk/develop/real-time-transcription/project-implementation/flutter.mdx new file mode 100644 index 000000000..673ffb705 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-implementation/flutter.mdx @@ -0,0 +1,311 @@ + + +### Implement the user interface + +To create a simple interface to test this feature, add buttons to start and query real-time transcription. Open `/lib/main.dart` and add the following code to the `build` method after `ListView(...children: [`: + +```dart +ElevatedButton( + onPressed: !_isJoined ? null : () => {realTimeTranscription()}, + child: isRealTimeTranscriptionRunning ? const Text("Stop real-time transcription") + : const Text("Start real-time transcription"), +), +ElevatedButton( + onPressed: (_isJoined && isRealTimeTranscriptionRunning) ? () => {query()} : null, + child: const Text("Query real-time transcription"), +), +``` + +You see errors in your IDE. This is because these widgets refer to variables and methods that you create later. + +### Handle the system logic + +1. **Integrate an `HTTP` client library into your project** + + To call `HTTP` methods that start, stop, and query a real-time transcription task, you add an `HTTP` client library to your project. Open `pubspec.yaml` and add the following line under `dependencies:` + + ```groovy + http: ^0.13.5 + ``` + +1. **Import the required classes** + + To use the relevant `HTTP` classes and process response data, add the following statements after the last `import` statement in `/lib/main.dart`: + + ```dart + import 'dart:typed_data'; + import 'dart:convert'; + import 'package:http/http.dart' as http; + ``` + +1. **Define variables to manage a real-time transcription task** + + In `/lib/main.dart`, add the following declarations to the `_MyAppState` class: + + ```dart + String baseUrl = "https://api.agora.io"; + String apiKey = ""; + String apiSecret = ""; + String instanceId = "Test-1"; + String authorizationHeader = "", builderToken = ""; + String realTimeTranscriptionTaskId = ""; // Returned by the start method + bool isRealTimeTranscriptionRunning = false; + ``` + +### Manage a real-time transcription task + +Take the following steps to start, stop, and query a real-time transcription task in your . + +1. **Get a `builderToken`** + + To acquire a `builderToken`, you: + + 1. Generate a Base64-encoded credential from the API key and secret associated with your project. + 1. Add this credential to the request header as a parameter named `Authorization`. + 1. In the request body, pass the `instanceID` that you use to identify the instance. + + To acquire a `builderToken`, add the following method to the `_MyAppState` class: + + ```dart + Future getBuilderToken() async { + String url = "$baseUrl/v1/projects/$appId/rtsc/speech-to-text/builderTokens"; + // Concatenate the key and secret and use base64 encoding + String plainCredentials = "$apiKey:$apiSecret"; + String base64Credentials = base64Encode(utf8.encode(plainCredentials)); + // Create authorization header + authorizationHeader = "Basic $base64Credentials"; + + // Send the http POST request + final response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + "Authorization": authorizationHeader + }, + body: jsonEncode({ + 'instanceId': instanceId, + }), + ); + + if (response.statusCode == 200) { + // Server returned an OK response, parse the JSON + Map json = jsonDecode(response.body); + builderToken = json["tokenName"]; + showMessage("builderToken received"); + } else { + Map json = jsonDecode(response.body); + String message = json["message"]; + showMessage("builderToken not received: $message"); + } + } + ``` + + A `builderToken` is usable for 5 minutes. After the time has expired, you must generate a new token. + +1. **Start a real-time transcription task** + + To start a real-time transcription task, you: + 1. Create a JSON configuration and pass it in the body of a `POST` request. + 1. Add the `builderToken` as a query parameter to the URL. + 1. Make a POST call to . + On success, the method returns a `taskId` that you use to query or stop the task. + + To start a task, add the following method to the `_MyAppState` class: + + ```dart + Future startRealTimeTranscription() async { + String url = "$baseUrl/v1/projects/$appId/rtsc/speech-to-text/tasks?builderToken=$builderToken"; + + // Specify real-time transcription-configuration + var config = { + 'audio': { + 'subscribeSource': 'AGORARTC', + 'agoraRtcConfig': { + 'channelName': channelName, + 'uid': uid.toString(), + 'token': token, + 'channelType': 'LIVE_TYPE', + 'subscribeConfig': {'subscribeMode': 'CHANNEL_MODE'}, + 'maxIdleTime': 60 + } + }, + 'config': { + 'features': ['RECOGNIZE'], + 'recognizeConfig': { + 'language': 'ENG', + 'model': 'Model', + 'output': { + 'destinations': ['AgoraRTCDataStream', 'Storage'], + 'agoraRTCDataStream': { + 'channelName': channelName, + 'uid': uid.toString(), + 'token': token, + }, + 'cloudStorage': [ + { + 'format': 'HLS', + 'storageConfig': { + 'accessKey': '', + 'secretKey': '', + 'bucket': '', + 'vendor': 0, + 'region': 0, + 'fileNamePrefix': ['directory', 'subDirectory'] + } + } + ] + } + } + } + }; + + // Build Request body JSON + String body = jsonEncode(config); + + // Send the http POST request + final response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + "Authorization": authorizationHeader + }, + body: body, + ); + + if (response.statusCode == 200) { + // Server returned an OK response, parse the JSON + Map json = jsonDecode(response.body); + String status = json["status"]; + realTimeTranscriptionTaskId = json["taskId"]; + if (status.compareTo("STARTED") == 0 || status.compareTo("IN_PROGRESS") == 0) { + setState((){ + isRealTimeTranscriptionRunning = true; + }); + } + showMessage("Real-time transcription task started"); + } else { + Map json = jsonDecode(response.body); + String message = json["message"]; + showMessage("Real-time transcription task could not be started: $message"); + } + } + ``` + +1. **Query task status** + + You query the status of a real-time transcription task using a `GET` request to notify the user of any change. The request URL contains the `taskId` and `builderToken`. To do this, add the following method to the `_MyAppState`class: + + ```dart + Future queryRealTimeTranscription () async { + String url = "$baseUrl/v1/projects/$appId/rtsc/speech-to-text/tasks/$realTimeTranscriptionTaskId?builderToken=$builderToken"; + + // Send the http GET request + final response = await http.get( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + "Authorization": authorizationHeader + }, + ); + + if (response.statusCode == 200) { + // Server returned an OK response, parse the JSON + Map json = jsonDecode(response.body); + String status = json["status"]; + realTimeTranscriptionTaskId = json["taskId"]; + + if (status.compareTo("IN_PROGRESS") == 0) { + setState(() { + isRealTimeTranscriptionRunning = true; + }); + showMessage("Real-time transcription is running"); + } else{ + setState(() { + isRealTimeTranscriptionRunning = false; + }); + realTimeTranscriptionTaskId = ""; + showMessage("Real-time transcription status: $status"); + } + } else { + Map json = jsonDecode(response.body); + String message = json["message"]; + showMessage("Exception querying real-time transcription task: $message"); + } + } + ``` + +1. **Stop the task** + + To stop real-time transcription, you send an `HTTP DELETE` request with the `appId`, `taskId`, and `builderToken` in the URL. In the `_MyAppState` class, add the following method: + + ```dart + Future stopRealTimeTranscription() async { + String url = "$baseUrl/v1/projects/$appId/rtsc/speech-to-text/tasks/$realTimeTranscriptionTaskId?builderToken=$builderToken"; + + // Send the http DELETE request + final response = await http.delete( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + "Authorization": authorizationHeader + }, + ); + + if (response.statusCode == 200) { + // Server returned an OK response, parse the JSON + showMessage("Real-time transcription task stopped"); + realTimeTranscriptionTaskId = ""; + setState(() { + isRealTimeTranscriptionRunning = false; + }); + } else { + Map json = jsonDecode(response.body); + String message = json["message"]; + showMessage("Exception stopping real-time transcription task: $message"); + } + } + ``` + +1. **Receive and display the text stream** + + When you set one of the `destinations` in the starting configuration as `AgoraRTCDataStream`, the real-time transcription conversion output stream is pushed to your channel and you receive it using `onStreamMessage`. + + To receive this stream in a callback and display the text in your , add the following code after `agoraEngine.registerEventHandler(RtcEngineEventHandler(` in `setupVideoSDKEngine()`: + + ```dart + onStreamMessage: (RtcConnection connection, int remoteUid, int streamId, + Uint8List data, int length, int sentTs) { + showMessage(utf8.decode(data)); + }, + ``` + +1. **Enable a user to start, query, and stop a task** + + In this example, you use buttons to manage a real-time transcription task. + + 1. When a user presses the first button, the requests a `builderToken`. If the request is successful, the starts a real-time transcription task. If a task is already running, you stop the task. To implement this functionality, add the following method to the `_MyAppState` class: + + ```dart + void realTimeTranscription() async { + if (!isRealTimeTranscriptionRunning) { + await getBuilderToken(); + startRealTimeTranscription(); + } else { + stopRealTimeTranscription(); + } + } + ``` + + 1. When a user presses the query status button, you call the query method to inform the user of the current task status and update the user interface. To implement this functionality, add the following method to the `_MyAppState` class. + + ```dart + void query() async { + if (isRealTimeTranscriptionRunning) { + queryRealTimeTranscription(); + } else { + showMessage("Real-time transcription task is not running"); + } + } + ``` + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-implementation/index.mdx b/shared/video-sdk/develop/real-time-transcription/project-implementation/index.mdx new file mode 100644 index 000000000..8885060ec --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-implementation/index.mdx @@ -0,0 +1,20 @@ +import Android from './android.mdx'; +import Ios from './ios.mdx'; +import Web from './web.mdx'; +import Unity from './unity.mdx'; +import ReactNative from './react-native.mdx'; +import Electron from './electron.mdx'; +import Flutter from './flutter.mdx'; +import MacOS from './macos.mdx'; +import Windows from './windows.mdx'; + + + + + + + + + + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-implementation/ios.mdx b/shared/video-sdk/develop/real-time-transcription/project-implementation/ios.mdx new file mode 100644 index 000000000..698148b6f --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-implementation/ios.mdx @@ -0,0 +1,384 @@ + + +### Implement the user interface + +To create a simple interface to test this feature, you add the following elements to the user interface: + +* A button to start and stop real-time transcription task +* A button to query the real-time transcription task status + +To create this user interface, in the `ViewController` class: + +1. **Add the UI elements you need** + + Add the following declarations at the top of the class. + + ```swift + var realTimeTranscriptionButton: UIButton! + var queryRealTimeTranscriptionButton: UIButton! + ``` + +1. **Configure the UI elements in your interface** + + Paste the following lines inside the `initViews` function: + + ```swift + // Button to start or stop the real-time transcription task + realTimeTranscriptionButton = UIButton(type: .system) + realTimeTranscriptionButton.frame = CGRect(x: 60, y: 750, width: 250, height: 20) + realTimeTranscriptionButton.setTitle("Start real-time transcription", for: .normal) + + realTimeTranscriptionButton.addTarget(self, action: #selector(realTimeTranscription), for: .touchUpInside) + self.view.addSubview(realTimeTranscriptionButton) + + // Button to query the status of the real-time transcription task + queryRealTimeTranscriptionButton = UIButton(type: .system) + queryRealTimeTranscriptionButton.frame = CGRect(x: 60, y: 780, width: 250, height: 20) + queryRealTimeTranscriptionButton.setTitle("Query real-time transcription", for: .normal) + + queryRealTimeTranscriptionButton.addTarget(self, action: #selector(query), for: .touchUpInside) + self.view.addSubview(queryRealTimeTranscriptionButton) + ``` + +You see errors in your IDE. This is because the layout refers to methods that you create later. + +### Handle the system logic + +1. **Define variables to manage a real-time transcription task** + + Add the following declarations to the top of the `ViewController` class: + + ```swift + let baseUrl = "https://api.agora.io" + var apiKey = "" + var apiSecret = "" + let instanceID = "Test-1" + var authorizationHeader = "" + var builderToken = "" + var realTimeTranscriptionTaskId = "" // Returned by the start method + var isRealTimeTranscriptionRunning: Bool = false + ``` + +### Manage a real-time transcription task + +Take the following steps to start, stop, and query a real-time transcription task in your . + +1. **Get a `builderToken`** + + To acquire a `builderToken`, you: + + 1. Generate a Base64-encoded credential from the API key and secret associated with your project. + 1. Add this credential to the request header as a parameter named `Authorization`. + 1. In the request body, pass the `instanceID` that you use to identify the instance. + + To acquire a `builderToken`, add the following method to the `ViewController` class: + + ```swift + func getBuilderToken() { + guard let url = URL(string: "\(baseUrl)/v1/projects/\(appID)/rtsc/speech-to-text/builderTokens") else { return } + + // Concatenate the key and secret and use base64 encoding + let plainCredentials = "\(apiKey):\(apiSecret)" + + let plainCredentialsUtf8 = plainCredentials.data(using: .utf8) + let base64Credentials: String? = plainCredentialsUtf8?.base64EncodedString(options: []) + + // Create authorization header + authorizationHeader = "Basic \(base64Credentials!)" + + // Create the request + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(authorizationHeader, forHTTPHeaderField: "Authorization") + + let body: [String: String] = [ + "instanceId": instanceID + ] + // Generate JSON data from the body object and set as the request's HTTP body + request.httpBody = try? JSONSerialization.data(withJSONObject: body) + + // Make the API call + let task = URLSession.shared.dataTask(with: request) { data, response, err in + guard let data = data, err == nil else { + return + } + + let responseJSON = try? JSONSerialization.jsonObject(with: data, options: []) + + if let responseDict = responseJSON as? [String: Any] { + // Check if a successful response code was retrieved + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + DispatchQueue.main.async { + self.showMessage(title: "Error", text: "Error receiving builder token: \(responseDict["message"]!)", delay: 0) + } + return + } + + if let tokenToReturn = responseDict["tokenName"] as? String { + // Key "tokenName" found in response, assigning to builderToken + self.builderToken = tokenToReturn + self.showMessage(title:"Success", text:"builderToken received", delay: 0) + self.startRealTimeTranscription() + } else { + self.showMessage(title: "Error", text: "builderToken not received: \(responseDict["message"]!)", delay: 0) + } + } else { + self.showMessage(title: "Error", text: "builderToken not received", delay: 0) + } + } + task.resume() + } + ``` + + A `builderToken` is usable for 5 minutes. After the time has expired, you must generate a new token. + +1. **Start a real-time transcription task** + + To start a real-time transcription task, you: + 1. Create a JSON configuration and pass it in the body of a `POST` request. + 1. Add the `builderToken` as a query parameter to the URL. + 1. Make a `POST` call to . + + To start a task, add the following method to the `ViewController` class: + + ```swift + func startRealTimeTranscription() { + guard let url = URL(string: "\(baseUrl)/v1/projects/\(appID)/rtsc/speech-to-text/tasks?builderToken=\(builderToken)") else { return } + + // Create the request + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(authorizationHeader, forHTTPHeaderField: "Authorization") + + let body: [String: Any] = [ + "audio": [ + "subscribeSource": "AGORARTC", + "agoraRtcConfig": [ + "channelName": channelName, + "uid": "", + "token": token, + "channelType": "LIVE_TYPE", + "subscribeConfig": [ + "subscribeMode": "CHANNEL_MODE" + ], + "maxIdleTime": 60 + ] + ], + "config": [ + "features": ["RECOGNIZE"], + "recognizeConfig": [ + "language": "ENG", + "model": "Model", + "output": [ + "destinations": [ + "AgoraRTCDataStream", + "Storage" + ], + "agoraRTCDataStream": [ + "channelName": channelName, + "uid": "", + "token": token + ], + "cloudStorage": [ + [ + "format": "HLS", + "storageConfig": [ + "accessKey": "", + "secretKey": "", + "bucket": "", + "vendor": 0, // Your Oss Vendor id + "region": 0, // Your Oss Region id + "fileNamePrefix": [ + "directory1", + "directory2" + ] + ] + ] + ] + ] + ] + ] + ] + + // Generate JSON data from the body object and set as the request's HTTP body + request.httpBody = try? JSONSerialization.data(withJSONObject: body) + + // Make the API call + let task = URLSession.shared.dataTask(with: request) { data, response, err in + guard let data = data, err == nil else { + return + } + + let responseJSON = try? JSONSerialization.jsonObject(with: data, options: []) + + if let responseDict = responseJSON as? [String: Any] { + // Check if a successful response code was retrieved + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + self.showMessage(title: "Error", text: "Error starting real-time transcription task: \(responseDict["message"]!)", delay: 0) + return + } + + if let status = responseDict["status"] as? String, let taskId = responseDict["taskId"] as? String { + self.realTimeTranscriptionTaskId = taskId + + if status == "STARTED" || status == "IN_PROGRESS" { + self.isRealTimeTranscriptionRunning = true + self.showMessage(title: "Success", text: "Real-time transcription task started", delay: 0) + DispatchQueue.main.async { + self.realTimeTranscriptionButton.setTitle("Stop real-time transcription", for: .normal) + } + } else { + self.isRealTimeTranscriptionRunning = false + self.showMessage(title: "Error", text: "Real-time transcription status: \(status)", delay: 0) + } + } else { + self.showMessage(title: "Error", text: "Real-time transcription status and/or taskId not received: \(responseDict["message"]!)", delay: 0) + } + } else { + self.showMessage(title: "Error", text: "Error parsing Start real-time transcription response", delay: 0) + } + } + task.resume() + } + ``` + +1. **Query task status** + + You query the status of a real-time transcription task using a `GET` request containing the `taskId` and `builderToken` to notify the user of any change. To do this, add the following method to the `ViewController` class: + + ```swift + func queryRealTimeTranscription() { + guard let url = URL(string: "\(baseUrl)/v1/projects/\(appID)/rtsc/speech-to-text/tasks/\(realTimeTranscriptionTaskId)?builderToken=\(builderToken)") else { return } + + // Create the request + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(authorizationHeader, forHTTPHeaderField: "Authorization") + + // Make the API call + let task = URLSession.shared.dataTask(with: request) { data, response, err in + guard let data = data, err == nil else { + return + } + let responseJSON = try? JSONSerialization.jsonObject(with: data, options: []) + + if let responseDict = responseJSON as? [String: Any] { + // Check if a successful response code was retrieved + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + DispatchQueue.main.async { + self.showMessage(title: "Error", text: "Error receiving query call response: \(responseDict["message"]!)", delay: 0) + } + return + } + + if let status = responseDict["status"] as? String, let taskId = responseDict["taskId"] as? String { + self.realTimeTranscriptionTaskId = taskId + + if status == "IN_PROGRESS" { + self.isRealTimeTranscriptionRunning = true + DispatchQueue.main.async { + self.realTimeTranscriptionButton.setTitle("Stop real-time transcription", for: .normal) + } + self.showMessage(title: "Success", text: "Real-time transcription is running", delay: 0) + } else { + self.isRealTimeTranscriptionRunning = false + self.realTimeTranscriptionTaskId = "" + self.showMessage(title: "Success", text: "Real-time transcription status: \(status)", delay: 0) + DispatchQueue.main.async { + self.realTimeTranscriptionButton.setTitle("Start real-time transcription", for: .normal) + } + } + } else { + self.showMessage(title: "Error", text: "Real-time transcription status and/or taskId not received: \(responseDict["message"]!)", delay: 0) + } + } else { + self.showMessage(title: "Error", text: "Error parsing query call response", delay: 0) + } + } + task.resume() + } + ``` + +1. **Stop the task** + + To stop real-time transcription, you send a `DELETE` request with the `appId`, `taskId`, and `builderToken` in the URL. In the `ViewController` class, add the following method: + + ```swift + func stopRealTimeTranscription() { + guard let url = URL(string: "\(baseUrl)/v1/projects/\(appID)/rtsc/speech-to-text/tasks/\(realTimeTranscriptionTaskId)?builderToken=\(builderToken)") else { return } + + // Create the request + var request = URLRequest(url: url) + request.httpMethod = "DELETE" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(authorizationHeader, forHTTPHeaderField: "Authorization") + + // Make the API call + let task = URLSession.shared.dataTask(with: request) { _, response, err in + guard err == nil else { + return + } + + if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { + DispatchQueue.main.async { + self.showMessage(title: "Success", text: "Real-time transcription task stopped", delay: 0) + self.realTimeTranscriptionButton.setTitle("Start real-time transcription", for: .normal) + } + self.realTimeTranscriptionTaskId = "" + self.isRealTimeTranscriptionRunning = false + } else { + DispatchQueue.main.async { + self.showMessage(title: "Error", text: "Error stopping real-time transcription task: \(response!)", delay: 0) + } + } + } + task.resume() + } + ``` + +1. **Receive and display the text stream** + + When you set one of the `destinations` in the starting configuration as `AgoraRTCDataStream`, the real-time transcription conversion output stream is pushed to your channel. + + To receive this stream and display the text in your , in `ViewController`, add the following function inside `extension ViewController: AgoraRtcEngineDelegate` along with the existing event handlers: + + ```swift + func rtcEngine(_ engine: AgoraRtcEngineKit, receiveStreamMessageFromUid uid: UInt, streamId: Int, data: Data) { + showMessage(title: "Success", text: "\(data)") + } + ``` + +1. **Enable a user to start, query, and stop a task** + + In this example, you use two buttons to manage a real-time transcription task. + + 1. When a user presses the first button, the requests a `builderToken`. If the request is successful, the starts a real-time transcription task. If a task is already running, you stop the task. To implement this functionality, add the following method to the `ViewController` class: + + ```swift + // Executed when real-time transcription button is clicked + @objc func realTimeTranscription() { + if (!isRealTimeTranscriptionRunning) { + getBuilderToken() + } else { + stopRealTimeTranscription() + } + } + ``` + + 1. When a user presses the query status button, you call the query method to inform the user of the current task status and update the user interface. To implement this functionality, add the following method to the `ViewController` class: + + ```swift + // Executed when the query real-time transcription button is clicked + @objc func query() { + if (isRealTimeTranscriptionRunning) { + queryRealTimeTranscription() + } else { + showMessage(title: "Error", text: "Real-time transcription task is not running", delay: 0) + } + } + ``` + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-implementation/macos.mdx b/shared/video-sdk/develop/real-time-transcription/project-implementation/macos.mdx new file mode 100644 index 000000000..ec028176c --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-implementation/macos.mdx @@ -0,0 +1,386 @@ + + +### Implement the user interface + +To create a simple interface to test this feature, you add the following elements to the user interface: + +* A button to start and stop real-time transcription task +* A button to query the real-time transcription task status + +To create this user interface, in the `ViewController` class: + +1. **Add the UI elements you need** + + Add the following declarations at the top of the class: + + ```swift + var realTimeTranscriptionButton: NSButton! + var queryRealTimeTranscriptionButton: NSButton! + ``` + +1. **Configure the UI elements in your interface** + + Paste the following lines inside the `initViews` function: + + ```swift + // Button to start or stop the real-time transcription task + realTimeTranscriptionButton = NSButton() + realTimeTranscriptionButton.frame = CGRect(x: 230, y: 240, width: 80, height: 20) + realTimeTranscriptionButton.title = "Start real-time transcription" + realTimeTranscriptionButton.target = self + + realTimeTranscriptionButton.action = #selector(realTimeTranscription) + self.view.addSubview(realTimeTranscriptionButton) + + // Button to query the status of the real-time transcription task + queryRealTimeTranscriptionButton = NSButton() + queryRealTimeTranscriptionButton.frame = CGRect(x: 310, y: 240, width: 80, height: 20) + queryRealTimeTranscriptionButton.title = "Query real-time transcription" + queryRealTimeTranscriptionButton.target = self + + queryRealTimeTranscriptionButton.action = #selector(query) + self.view.addSubview(queryRealTimeTranscriptionButton) + ``` + +You see errors in your IDE. This is because the layout refers to methods that you create later. + +### Handle the system logic + +1. **Define variables to manage a real-time transcription task** + + Add the following declarations to the top of the `ViewController` class: + + ```swift + let baseUrl = "https://api.agora.io" + var apiKey = "" + var apiSecret = "" + let instanceID = "Test-1" + var authorizationHeader = "" + var builderToken = "" + var realTimeTranscriptionTaskId = "" // Returned by the start method + var isRealTimeTranscriptionRunning: Bool = false + ``` + +### Manage a real-time transcription task + +Take the following steps to start, stop, and query a real-time transcription task in your . + +1. **Get a `builderToken`** + + To acquire a `builderToken`, you: + + 1. Generate a Base64-encoded credential from the API key and secret associated with your project. + 1. Add this credential to the request header as a parameter named `Authorization`. + 1. In the request body, pass the `instanceID` that you use to identify the instance. + + To acquire a `builderToken`, add the following method to the `ViewController` class: + + ```swift + func getBuilderToken() { + guard let url = URL(string: "\(baseUrl)/v1/projects/\(appID)/rtsc/speech-to-text/builderTokens") else { return } + + // Concatenate the key and secret and use base64 encoding + let plainCredentials = "\(apiKey):\(apiSecret)" + + let plainCredentialsUtf8 = plainCredentials.data(using: .utf8) + let base64Credentials: String? = plainCredentialsUtf8?.base64EncodedString(options: []) + + // Create authorization header + authorizationHeader = "Basic \(base64Credentials!)" + + // Create the request + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(authorizationHeader, forHTTPHeaderField: "Authorization") + + let body: [String: String] = [ + "instanceId": instanceID + ] + // Generate JSON data from the body object and set as the request's HTTP body + request.httpBody = try? JSONSerialization.data(withJSONObject: body) + + // Make the API call + let task = URLSession.shared.dataTask(with: request) { data, response, err in + guard let data = data, err == nil else { + return + } + + let responseJSON = try? JSONSerialization.jsonObject(with: data, options: []) + + if let responseDict = responseJSON as? [String: Any] { + // Check if a successful response code was retrieved + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + DispatchQueue.main.async { + self.showMessage(title: "Error", text: "Error receiving builder token: \(responseDict["message"]!)", delay: 0) + } + return + } + + if let tokenToReturn = responseDict["tokenName"] as? String { + // Key "tokenName" found in response, assigning to builderToken + self.builderToken = tokenToReturn + self.showMessage(title:"Success", text:"builderToken received", delay: 0) + self.startRealTimeTranscription() + } else { + self.showMessage(title: "Error", text: "builderToken not received: \(responseDict["message"]!)", delay: 0) + } + } else { + self.showMessage(title: "Error", text: "builderToken not received", delay: 0) + } + } + task.resume() + } + ``` + + A `builderToken` is usable for 5 minutes. After the time has expired, you must generate a new token. + +1. **Start a real-time transcription task** + + To start a real-time transcription task, you: + 1. Create a JSON configuration and pass it in the body of a `POST` request. + 1. Add the `builderToken` as a query parameter to the URL. + 1. Make a `POST` call to . + + To start a task, add the following method to the `ViewController` class: + + ```swift + func startRealTimeTranscription() { + guard let url = URL(string: "\(baseUrl)/v1/projects/\(appID)/rtsc/speech-to-text/tasks?builderToken=\(builderToken)") else { return } + + // Create the request + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(authorizationHeader, forHTTPHeaderField: "Authorization") + + let body: [String: Any] = [ + "audio": [ + "subscribeSource": "AGORARTC", + "agoraRtcConfig": [ + "channelName": channelName, + "uid": "", + "token": token, + "channelType": "LIVE_TYPE", + "subscribeConfig": [ + "subscribeMode": "CHANNEL_MODE" + ], + "maxIdleTime": 60 + ] + ], + "config": [ + "features": ["RECOGNIZE"], + "recognizeConfig": [ + "language": "ENG", + "model": "Model", + "output": [ + "destinations": [ + "AgoraRTCDataStream", + "Storage" + ], + "agoraRTCDataStream": [ + "channelName": channelName, + "uid": "", + "token": token + ], + "cloudStorage": [ + [ + "format": "HLS", + "storageConfig": [ + "accessKey": "", + "secretKey": "", + "bucket": "", + "vendor": 0, // Your Oss Vendor id + "region": 0, // Your Oss Region id + "fileNamePrefix": [ + "directory1", + "directory2" + ] + ] + ] + ] + ] + ] + ] + ] + + // Generate JSON data from the body object and set as the request's HTTP body + request.httpBody = try? JSONSerialization.data(withJSONObject: body) + + // Make the API call + let task = URLSession.shared.dataTask(with: request) { data, response, err in + guard let data = data, err == nil else { + return + } + + let responseJSON = try? JSONSerialization.jsonObject(with: data, options: []) + + if let responseDict = responseJSON as? [String: Any] { + // Check if a successful response code was retrieved + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + self.showMessage(title: "Error", text: "Error starting real-time transcription task: \(responseDict["message"]!)", delay: 0) + return + } + + if let status = responseDict["status"] as? String, let taskId = responseDict["taskId"] as? String { + self.realTimeTranscriptionTaskId = taskId + + if status == "STARTED" || status == "IN_PROGRESS" { + self.isRealTimeTranscriptionRunning = true + self.showMessage(title: "Success", text: "Real-time transcription task started", delay: 0) + DispatchQueue.main.async { + self.realTimeTranscriptionButton.title = "Stop real-time transcription" + } + } else { + self.isRealTimeTranscriptionRunning = false + self.showMessage(title: "Error", text: "Real-time transcription status: \(status)", delay: 0) + } + } else { + self.showMessage(title: "Error", text: "Real-time transcription status and/or taskId not received: \(responseDict["message"]!)", delay: 0) + } + } else { + self.showMessage(title: "Error", text: "Error parsing Start real-time transcription response", delay: 0) + } + } + task.resume() + } + ``` + +1. **Query task status** + + You query the status of a real-time transcription task using a `GET` request containing the `taskId` and `builderToken` to notify the user of any change. To do this, add the following method to the `ViewController` class: + + ```swift + func queryRealTimeTranscription() { + guard let url = URL(string: "\(baseUrl)/v1/projects/\(appID)/rtsc/speech-to-text/tasks/\(realTimeTranscriptionTaskId)?builderToken=\(builderToken)") else { return } + + // Create the request + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(authorizationHeader, forHTTPHeaderField: "Authorization") + + // Make the API call + let task = URLSession.shared.dataTask(with: request) { data, response, err in + guard let data = data, err == nil else { + return + } + let responseJSON = try? JSONSerialization.jsonObject(with: data, options: []) + + if let responseDict = responseJSON as? [String: Any] { + // Check if a successful response code was retrieved + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + DispatchQueue.main.async { + self.showMessage(title: "Error", text: "Error receiving query call response: \(responseDict["message"]!)", delay: 0) + } + return + } + + if let status = responseDict["status"] as? String, let taskId = responseDict["taskId"] as? String { + self.realTimeTranscriptionTaskId = taskId + + if status == "IN_PROGRESS" { + self.isRealTimeTranscriptionRunning = true + DispatchQueue.main.async { + self.realTimeTranscriptionButton.title = "Stop real-time transcription" + } + self.showMessage(title: "Success", text: "Real-time transcription is running", delay: 0) + } else { + self.isRealTimeTranscriptionRunning = false + self.realTimeTranscriptionTaskId = "" + self.showMessage(title: "Success", text: "Real-time transcription status: \(status)", delay: 0) + DispatchQueue.main.async { + self.realTimeTranscriptionButton.title = "Start real-time transcription" + } + } + } else { + self.showMessage(title: "Error", text: "Real-time transcription status and/or taskId not received: \(responseDict["message"]!)", delay: 0) + } + } else { + self.showMessage(title: "Error", text: "Error parsing query call response", delay: 0) + } + } + task.resume() + } + ``` + +1. **Stop the task** + + To stop real-time transcription, you send a `DELETE` request with the `appId`, `taskId`, and `builderToken` in the URL. In the `ViewController` class, add the following method: + + ```swift + func stopRealTimeTranscription() { + guard let url = URL(string: "\(baseUrl)/v1/projects/\(appID)/rtsc/speech-to-text/tasks/\(realTimeTranscriptionTaskId)?builderToken=\(builderToken)") else { return } + + // Create the request + var request = URLRequest(url: url) + request.httpMethod = "DELETE" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(authorizationHeader, forHTTPHeaderField: "Authorization") + + // Make the API call + let task = URLSession.shared.dataTask(with: request) { _, response, err in + guard err == nil else { + return + } + + if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { + DispatchQueue.main.async { + self.showMessage(title: "Success", text: "Real-time transcription task stopped", delay: 0) + self.realTimeTranscriptionButton.title = "Start real-time transcription" + } + self.realTimeTranscriptionTaskId = "" + self.isRealTimeTranscriptionRunning = false + } else { + DispatchQueue.main.async { + self.showMessage(title: "Error", text: "Error stopping real-time transcription task: \(response!)", delay: 0) + } + } + } + task.resume() + } + ``` + +1. **Receive and display the text stream** + + When you set one of the `destinations` in the starting configuration as `AgoraRTCDataStream`, the real-time transcription conversion output stream is pushed to your channel. + + To receive this stream and display the text in your , in `ViewController`, add the following function inside `extension ViewController: AgoraRtcEngineDelegate` along with the existing event handlers: + + ```swift + func rtcEngine(_ engine: AgoraRtcEngineKit, receiveStreamMessageFromUid uid: UInt, streamId: Int, data: Data) { + showMessage(title: "Success", text: "\(data)") + } + ``` + +1. **Enable a user to start, query, and stop a task** + + In this example, you use two buttons to manage a real-time transcription task. + + 1. When a user presses the first button, the requests a `builderToken`. If the request is successful, the starts a real-time transcription task. If a task is already running, you stop the task. To implement this functionality, add the following method to the `ViewController` class: + + ```swift + // Executed when real-time transcription button is clicked + @objc func realTimeTranscription(sender: NSButton!) { + if (!isRealTimeTranscriptionRunning) { + getBuilderToken() + } else { + stopRealTimeTranscription() + } + } + ``` + + 1. When a user presses the query status button, you call the query method to inform the user of the current task status and update the user interface. To implement this functionality, add the following method to the `ViewController` class: + + ```swift + // Executed when the query real-time transcription button is clicked + @objc func query(sender: NSButton!) { + if (isRealTimeTranscriptionRunning) { + queryRealTimeTranscription() + } else { + showMessage(title: "Error", text: "Real-time transcription task is not running", delay: 0) + } + } + ``` + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-implementation/react-native.mdx b/shared/video-sdk/develop/real-time-transcription/project-implementation/react-native.mdx new file mode 100644 index 000000000..47482b3d8 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-implementation/react-native.mdx @@ -0,0 +1,338 @@ + + +### Implement the user interface + +To create a simple interface to test this feature, add `Start` and `Query` buttons to the UI. + +1. Open `App.tsx` and add the following lines after `Leave`: + + ```typescript + + Start real-time transcription + + + Query real-time transcription + + ``` + +### Handle the system logic + +To integrate HTTP and JSON capabilities into your for real-time transcription: + +1. **Import the `https` module into your project** + + To call `HTTP` methods that start, stop, and query a real-time transcription task, you add an `https` module to your project. Open `App.tsx` and add the following with the declarations: + + ```javascript + var https = require('https'); + ``` +1. **Install and Import the necessary modules** + + To convert your credentials to base-64 format, use the `Buffer` module. For text decoding, use the `TextDecoder` + module. To add these modules: + + 1. Run the following command in your terminal: + + ```bash + npm install buffer + npm install text-decoding + ``` + + 1. In `App.tsx`, add the following after the import statements: + + ```typescript + import {Buffer} from 'buffer'; + import {TextDecoder} from 'text-decoding' + ``` + +1. **Define variables to manage real-time transcription tasks** + + In `App.tsx`, add the following to the declarations: + + ```typescript + var baseUrl = "api.agora.io"; + var apiKey = ""; + var apiSecret = ""; + var instanceID = "Test-1"; + var authorizationHeader, builderToken; + var realTimeTranscriptionTaskId = ""; // Returned by the start method + var isRealTimeTranscriptionRunning = false; + ``` + +### Manage a real-time transcription task + +Take the following steps to start, stop, and query a real-time transcription task in your . + +1. **Get a `builderToken`** + + To acquire a `builderToken`, you: + + 1. Generate a Base64-encoded credential from the API key and secret associated with your project. + 1. Add this credential to the request header as a parameter named `Authorization`. + 1. In the request body, pass the `instanceID` that you use to identify the instance. + + To acquire a `builderToken`, in `App.tsx`, add the following before `const join = async () => {}`: + + ```typescript + const realTimeTranscription = () => { + if (isRealTimeTranscriptionRunning) { + stopRealTimeTranscription(); + return; + } + var path = '/v1/projects/' + appId + '/rtsc/speech-to-text/builderTokens'; + var plainCredentials = apiKey + ':' + apiSecret; + var base64Credentials = Buffer.from(plainCredentials).toString('base64'); + authorizationHeader = 'Basic ' + base64Credentials; + var postData = JSON.stringify({ + instanceId: instanceID, + }); + // Set request parameters + const options = { + hostname: baseUrl, + port: 443, + path: path, + method: 'POST', + headers: { + Authorization: authorizationHeader, + 'Content-Type': 'application/json', + 'Content-Length': postData.length, + }, + }; + // Create a request object and send request. + const req = https.request(options, (res: any) => { + console.log(`Status code: ${res.statusCode}`); + res.on('data', function (data: any) { + // Parse the response to extract the token. + var data = JSON.parse(data); + builderToken = data.tokenName; + startRealTimeTranscription(); + console.log(builderToken); + }); + }); + req.on('error', (error: Error) => { + // Display the error result when the request fails. + console.error(error); + }); + // Send the post request data to the upstream server. + req.write(postData); + // End the request. + req.end(); + }; + ``` + + A `builderToken` is usable for 5 minutes. After the time has expired, you must generate a new token. + +1. **Start a real-time transcription task** + + To start a real-time transcription task, you: + 1. Create a JSON configuration and pass it in the body of a `POST` request. + 1. Add the `builderToken` as a query parameter to the URL. + 1. Make a `POST` call to . + On success, the method returns a `taskId` that you use to query or stop the task. + + To start a task, in `App.tsx`, add the following method before `const join = async () => {}`: + + ```typescript + const startRealTimeTranscription = () => { + var url = + '/v1/projects/' + + appId + + '/rtsc/speech-to-text/tasks?' + + 'builderToken=' + + builderToken; + // Setup a JSON string to specify the real-time transcription configuration. + var body = JSON.stringify({ + audio: { + subscribeSource: 'AGORARTC', + agoraRtcConfig: { + channelName: channelName, + uid: '1', + token: token, + channelType: 'LIVE_TYPE', + subscribeConfig: { + subscribeMode: 'CHANNEL_MODE', + }, + maxIdleTime: 60, + }, + }, + config: { + features: ['RECOGNIZE'], + recognizeConfig: { + language: 'ENG', + model: 'Model', + output: { + destinations: ['AgoraRTCDataStream', 'Storage'], + agoraRTCDataStream: { + channelName: channelName, + uid: '1', + token: token, + }, + cloudStorage: [ + { + format: 'HLS', + storageConfig: { + vendor: 0, + region: 0, + bucket: '', + accessKey: '', + secretKey: '', + }, + fileNamePrefix: ['directory1', 'directory2'], + }, + ], + }, + }, + }, + }); + const options = { + hostname: baseUrl, + path: url, + method: 'POST', + headers: { + Authorization: authorizationHeader, + 'Content-Type': 'application/json', + 'Content-Length': body.length, + }, + }; + // Create request object and send request + const req = https.request(options, (res: any) => { + console.log(`Status code: ${res.statusCode}`); + res.on('data', (d: any) => { + // Parse the response to learn about the status of the real-time transcription task. + var data = JSON.parse(d); + var status = data.status; + realTimeTranscriptionTaskId = data.taskId; + if (status === 'STARTED' || status === 'IN_PROGRESS') { + isRealTimeTranscriptionRunning = true; + realTimeTranscriptionTaskId = data.taskId; + console.log('real-time transcription task started'); + } else { + isRealTimeTranscriptionRunning = false; + console.log('real-time transcription status: ' + status); + } + }); + }); + req.on('error', (error: any) => { + // Display the error result when the request fails. + console.error('Error parsing query call response: ' + error); + }); + // Send the post request data to the upstream server. + req.write(body); + // End the request. + req.end(); + }; + ``` + +1. **Query task status** + + To notify the user of any change to the real-time transcription task, you query the task status using a `GET` request containing the `taskId` and `builderToken`. + + In `App.tsx`, add the following method before `const join = async () => {}`: + + ```typescript + const realTimeTranscriptionQuery = () => { + var path = + '/v1/projects/' + + appId + + '/rtsc/speech-to-text/tasks/' + + realTimeTranscriptionTaskId + + '?builderToken=' + + builderToken; + // Set request parameters + const options = { + hostname: baseUrl, + port: 443, + path: path, + method: 'GET', + headers: { + Authorization: authorizationHeader, + 'Content-Type': 'application/json', + }, + }; + // Create a request object and send request + const req = https.request(options, (res: any) => { + console.log(`Status code: ${res.statusCode}`); + res.on('data', function (data: any) { + // Parse the response to extract the query results. + var data = JSON.parse(data); + var status = data.status; + realTimeTranscriptionTaskId = data.taskId; + if (status === 'IN_PROGRESS') { + isRealTimeTranscriptionRunning = true; + console.log('real-time transcriptionis running'); + } else { + isRealTimeTranscriptionRunning = false; + realTimeTranscriptionTaskId = ''; + console.log('real-time transcriptionstatus: ' + status + ''); + } + }); + }); + req.on('error', (error: any) => { + // Display the error result when the request fails. + console.error(error); + }); + // End the request. + req.end(); + }; + ``` + +1. **Stop the task** + + To stop real-time transcription, you send a `DELETE` request with the `appId`, `taskId`, and `builderToken` in the URL. + + In the `App.tsx`, add the following method before `const join = async () => {}`: + + ```typescript + const stopRealTimeTranscription = () => { + var path = + '/v1/projects/' + + appId + + '/rtsc/speech-to-text/tasks/' + + realTimeTranscriptionTaskId + + '?builderToken=' + + builderToken; + const options = { + hostname: baseUrl, + port: 443, + path: path, + method: 'DELETE', + headers: { + Authorization: authorizationHeader, + 'Content-Type': 'application/json', + }, + }; + // Create a request object and send request + const req = https.request(options, (res: any) => { + console.log(`Status code: ${res.statusCode}`); + if (res.statusCode === '200') { + console.log('Real-time transcription task stopped'); + realTimeTranscriptionTaskId = ''; + isRealTimeTranscriptionRunning = false; + } + res.on('data', (d: any) => { + console.log(d); + }); + }); + req.on('error', (error: any) => { + // Display the error result when the request fails. + console.error('Error while stopping the real-time transcription task: ' + error); + }); + // End the request. + req.end(); + }; + ``` + +1. **Receive and display the text stream** + + When you set one of the `destinations` in the starting configuration as `AgoraRTCDataStream`, the real-time transcription conversion output stream is pushed to your channel and you receive it using `onStreamMessage`. + + To receive this stream and display the text in your , in `App.tsx`, add the following code inside `agoraEngine.registerEventHandler({})`: + + ```typescript + onStreamMessage: (connection, _remoteUid, streamId, data, _length) => { + var text = new TextDecoder().decode(data); + console.log(text); + }, + ``` + + \ No newline at end of file diff --git a/shared/video-sdk/develop/real-time-transcription/project-implementation/unity.mdx b/shared/video-sdk/develop/real-time-transcription/project-implementation/unity.mdx new file mode 100644 index 000000000..b80b052fe --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-implementation/unity.mdx @@ -0,0 +1,372 @@ + + +### Implement the user interface + +To create a simple interface to test this feature, you add two buttons to the UI. + + 1. In **Sample Scene**, right-click **Canvas**, then click **UI** > **Button - TextMeshPro**. A button appears in the **Scene** Canvas. + + 2. In **Inspector**, rename **Button** to `realTimeTranscription`, and then change the following coordinates: + + * **Pos X** - -140 + * **Pos Y** - -172 + * **Width** - 250 + + 3. Select the **Text(TMP)** sub-item of `realTimeTranscription`, and in **Inspector**, change **Text** to *Start transcription*. + + 4. Use the same procedure to create a button called `queryRealTimeTranscription` with a **Text(TMP)** sub-item where its **Text** says *Query real-time transcription*. + + 5. To adjust the position and size of `queryRealTimeTranscription`, in **Inspector**, change the following coordinates: + + * **Pos X** - 110 + * **Pos Y** - -172 + * **Width** - 240 + +### Handle the system logic + +To integrate HTTP and JSON capabilities into your for real-time transcription: + +1. **Add the required namespaces** + + To start, stop, and query a real-time transcription task, in your script file, add the following to the list of namespaces: + + ```csharp + using System.IO; + using System.Net; + using System.Text; + using System; + ``` + +1. **Define variables to manage a real-time transcription task** + + In your script file, add the following declarations to `NewBehaviourScript`: + + ```csharp + private string baseUrl = "https://api.agora.io"; + private string apiKey = ""; + private string apiSecret = ""; + private string instanceID = "Test-1"; + private string authorizationHeader, builderToken; + private string realTimeTranscriptionTaskId = ""; // Returned by the start method. + private bool isRealTimeTranscriptionRunning = false; + int Uid = 1; + ``` + +### Manage a real-time transcription task + +Take the following steps to start, stop, and query a real-time transcription task in your . + +1. **Get a `builderToken`** + + To acquire a `builderToken`, you: + + 1. Generate a Base64-encoded credential from the API key and secret associated with your project. + 1. Add this credential to the request header as a parameter named `Authorization`. + 1. In the request body, pass the `instanceID` that you use to identify the instance. + + To acquire a `builderToken`, in your script file, add the following method to `NewBehaviourScript`: + + ```csharp + public void fetchBuilderToken() + { + string url = baseUrl + "/v1/projects/"+ _appID +"/rtsc/speech-to-text/builderTokens"; + // Concatenate customer key and customer secret and use base64 to encode the concatenated string + string plainCredentials = apiKey + ":" + apiSecret; + // Encode the credentials to base64 + var plainTextBytes = Encoding.UTF8.GetBytes(plainCredentials); + string encodedCredential = Convert.ToBase64String(plainTextBytes); + // Create an authorization header. + authorizationHeader = "Authorization: Basic " + encodedCredential; + // Create a request object. + WebRequest request = WebRequest.Create(url); + // Specify the request method. + request.Method = "POST"; + Debug.Log(authorizationHeader); + // Add the authorization header to the request. + request.Headers.Add(authorizationHeader); + request.ContentType = "application/json"; + using (var streamWriter = new StreamWriter(request.GetRequestStream())) + { + string json = @" + { + ""instanceId"":""" + instanceID + @""" + }"; + streamWriter.Write(json); + } + // Send request and receive a response. + WebResponse response = request.GetResponse(); + using (Stream dataStream = response.GetResponseStream()) + { + // Read the response and extract the token. + StreamReader reader = new StreamReader(dataStream); + string responseFromServer = reader.ReadToEnd(); + Debug.Log(responseFromServer); + if(((HttpWebResponse)response).StatusDescription != "OK") + { + Debug.Log("Request failed! Make sure request parameters are correct"); + return; + } + responseFromServer = responseFromServer.Replace("{", ""); + responseFromServer = responseFromServer.Replace("}", ""); + String[] list1 = responseFromServer.Split(","); + String[] list2 = list1[2].Split(":"); + builderToken = list2[1]; + builderToken = builderToken.Replace("\"", ""); + Debug.Log(builderToken); + } + response.Close(); + } + ``` + To execute this method at startup, in your script file, add the following at the end of `SetupVideoSDKEngine`: + + ```csharp + fetchBuilderToken(); + ``` + A `builderToken` is usable for 5 minutes. After the time has expired, you must generate a new token. + +1. **Start a real-time transcription task** + + To start a real-time transcription task, you: + 1. Create a JSON configuration and pass it in the body of a `POST` request. + 1. Add the `builderToken` as a query parameter to the URL. + 1. Make a `POST` call to . + On success, the method returns a `taskId` that you use to query or stop the task. + + To start a task, in your script file, add the following method to `NewBehaviourScript`: + + ```csharp + public void startRealTimeTranscription() + { + if(isRealTimeTranscriptionRunning) + { + stopRealTimeTranscription(); + return; + } + string url = baseUrl + "/v1/projects/" + _appID + "/rtsc/speech-to-text/tasks?" + "builderToken=" + builderToken; + Debug.Log(url); + // Create a request object + WebRequest request = WebRequest.Create(url); + // Specify the request type. + request.Method = "POST"; + // Add the authorization header. + request.Headers.Add(authorizationHeader); + request.ContentType = "application/json"; + using (var streamWriter = new StreamWriter(request.GetRequestStream())) + { + // Setup a JSON string to specify a real-time transcription configuration. + string json = @" + { + ""audio"": + { + ""subscribeSource"": ""AGORARTC"", + ""agoraRtcConfig"": + { + ""channelName"": """ + _channelName + @""", + ""uid"": """ + Uid + @""", + ""token"": """ + _token + @""", + ""channelType"": ""LIVE_TYPE"", + ""subscribeConfig"": + { + ""subscribeMode"": ""CHANNEL_MODE"" + }, + ""maxIdleTime"": 60 + } + }, + ""config"": + { + ""features"": + [ + ""RECOGNIZE"" + ], + ""recognizeConfig"": + { + ""language"": ""ENG"", + ""model"": ""Model"", + ""output"": + { + ""destinations"": + [ + ""AgoraRTCDataStream"", + ""Storage"" + ], + ""agoraRTCDataStream"": + { + ""channelName"": """ + _channelName + @""", + ""uid"": """ + Uid + @""", + ""token"": """ + _token + @""" + }, + ""cloudStorage"": + [ + { + ""format"": ""HLS"", + ""storageConfig"": + { + ""vendor"": 0, + ""region"": 0, + ""bucket"":"""", + ""accessKey"":"""", + ""secretKey"":"""" + }, + ""fileNamePrefix"": + [ + ""directory1"", + ""directory2"" + ] + } + ] + } + } + } + }"; + streamWriter.Write(json); + } + WebResponse response = request.GetResponse(); + using (Stream dataStream = response.GetResponseStream()) + { + // Read response and learn about the status of the real-time transcription task. + StreamReader reader = new StreamReader(dataStream); + string responseFromServer = reader.ReadToEnd(); + Debug.Log(responseFromServer); + if(((HttpWebResponse)response).StatusDescription != "OK") + { + Debug.Log("Request failed! Make sure request parameters are correct"); + return; + } + responseFromServer = responseFromServer.Replace("{", ""); + responseFromServer = responseFromServer.Replace("}", ""); + String[] list1 = responseFromServer.Split(","); + String[] tempList= list1[1].Split(":"); + string status = tempList[1]; + status = status.Replace("\"", ""); + if(status == "STARTED"|| status == "IN_PROGRESS") + { + Debug.Log("Real-time transcription service has been enabled"); + String[] list2 = list1[2].Split(":"); + realTimeTranscriptionTaskId = list2[1]; + realTimeTranscriptionTaskId = realTimeTranscriptionTaskId.Replace("\"", ""); + isRealTimeTranscriptionRunning = true; + } + else + { + isRealTimeTranscriptionRunning = false; + realTimeTranscriptionTaskId = ""; + Debug.Log("Real-time transcription status: " + status + ""); + } + } + response.Close(); + } + ``` + +1. **Query task status** + + You query the status of a real-time transcription task using a `GET` request containing the `taskId` and `builderToken` to notify the user of any change. To do this, in your script file, add the following method to `NewBehaviourScript`: + + ```csharp + public void queryRealTimeTranscription() + { + string url = baseUrl + "/v1/projects/" + _appID + + "/rtsc/speech-to-text/tasks/" + realTimeTranscriptionTaskId + + "?builderToken=" + builderToken; + // Create a request object. + WebRequest request = WebRequest.Create(url); + // Specify the request type. + request.Method = "GET"; + // Add the authorization header. + request.Headers.Add(authorizationHeader); + request.ContentType = "application/json"; + WebResponse response = request.GetResponse(); + using (Stream dataStream = response.GetResponseStream()) + { + // Parse the response to get the query results. + StreamReader reader = new StreamReader(dataStream); + string responseFromServer = reader.ReadToEnd(); + Debug.Log(responseFromServer); + if(((HttpWebResponse)response).StatusDescription != "OK") + { + Debug.Log("Request failed! Make sure request parameters are correct"); + return; + } + responseFromServer = responseFromServer.Replace("{", ""); + responseFromServer = responseFromServer.Replace("}", ""); + String[] list1 = responseFromServer.Split(","); + String[] tempList= list1[1].Split(":"); + string status = tempList[1]; + status = status.Replace("\"", ""); + if(status == "STARTED"|| status == "IN_PROGRESS") + { + Debug.Log("Real-time transcription service is running"); + isRealTimeTranscriptionRunning = true; + } + else + { + isRealTimeTranscriptionRunning = false; + realTimeTranscriptionTaskId = ""; + Debug.Log("Real-time transcription service is not running: " + status + ""); + } + } + response.Close(); + } + ``` + +1. **Stop the task** + + To stop real-time transcription, you send a `DELETE` request with the `appId`, `taskId`, and `builderToken` in the URL. In your script file, add the following method to `NewBehaviourScript`: + + ```csharp + public void stopRealTimeTranscription() + { + String url = baseUrl + "/v1/projects/" + _appID + + "/rtsc/speech-to-text/tasks/" + realTimeTranscriptionTaskId + + "?builderToken=" + builderToken; + // Create request object + WebRequest request = WebRequest.Create(url); + // Specify the request type. + request.Method = "DELETE"; + // Add the authorization header. + request.Headers.Add(authorizationHeader); + request.ContentType = "application/json"; + WebResponse response = request.GetResponse(); + if(((HttpWebResponse)response).StatusDescription != "OK") + { + Debug.Log("Request failed! Make sure request parameters are correct"); + return; + } + if(((HttpWebResponse)response).StatusDescription == "OK") + { + Debug.Log("Real-time transcription task stopped"); + isRealTimeTranscriptionRunning = false; + realTimeTranscriptionTaskId = ""; + } + else + { + Debug.Log("Error while stopping real-time transcription:" + ((HttpWebResponse)response).StatusDescription); + } + response.Close(); + } + ``` +1. **Receive and display the text stream** + + When you set one of the `destinations` in the starting configuration as `AgoraRTCDataStream`, the real-time transcription conversion output stream is pushed to your channel and you receive it using `OnStreamMessage`. + + To receive this stream and display the text in your , in your script file, add the following code before `public override void OnUserJoined(RtcConnection connection, uint uid, int elapsed)`: + + ```csharp + public override void OnStreamMessage(RtcConnection connection, uint remoteUid, int streamId, byte[] data, uint length, UInt64 sentTs) + { + Debug.Log(Encoding.Default.GetString(data)); + } + ``` + +1. **Enable a user to start, query, and stop a task** + + In this example, you use buttons to manage a real-time transcription task. When a user presses the real-time transcription button, the calls `startRealTimeTranscription` and starts a real-time transcription task using the `builderToken`. If a task is already running, the stops the task. When the user presses the query button, the calls `queryRealTimeTranscription` and informs you of the current status of the real-time transcription task. To implement this logic, in your script file, add the following at the end of `SetupUI`: + + ```csharp + go = GameObject.Find("realTimeTranscription"); + go.GetComponent`: + ```html + + + ``` + +### Handle the system logic + +To integrate HTTP and JSON capabilities into your for real-time transcription: + +1. **Install the necessary modules into your project** + + To call `HTTP` methods that start, stop, and query a real-time transcription task, you add the `axios` module to your project. To generate the base-64 authorization header, you add the `Buffer` module. + + In Terminal, run the following commands: + + ```bash + npm install Buffer + npm install axios + ``` + +1. **Import the modules into your project** + + In `main.js`, add the following import statements: + + ```bash + import { Buffer } from "buffer/"; // the trailing slash is important! + import axios from "axios"; + ``` + +1. **Define variables to manage real-time transcription tasks** + + In `main.js`, add the following declarations after the import statements: + + ```java + // Pass your App ID here. + const appId = ''; + // Set the channel name. + const channel = ''; + // Pass your temp token here. + const token = ''; + // Set the user ID. + const uid = 1; + var baseUrl = "https://api.agora.io"; + var apiKey = ""; + var apiSecret = ""; + var instanceID = "Test-1"; + var authorizationHeader, builderToken; + var realTimeTranscriptionTaskId = ""; // Returned by the start method + var isRealTimeTranscriptionRunning = false; + ``` + +### Manage a real-time transcription task + +To start, stop, and query a real-time transcription task in your , do the following. + +1. **Get a `builderToken`** + + To acquire a `builderToken`, you: + + 1. Generate a Base64-encoded credential from the API key and secret associated with your project. + 1. Add this credential to the request header as a parameter named `Authorization`. + 1. In the request body, pass the `instanceID` that you use to identify the instance. + + In `main.js`, add the following before `document.getElementById("join").onclick = async function ()`: + + ```javascript + document.getElementById("realTimeTranscription").onclick = async function () + { + if(isRealTimeTranscriptionRunning) + { + stopRealTimeTranscription(); + return; + } + var path = "/v1/projects/" + appId + "/rtsc/speech-to-text/builderTokens"; + var plainCredentials = apiKey + ":" + apiSecret; + console.log(plainCredentials) + var base64Credentials = Buffer.from(plainCredentials).toString('base64') + authorizationHeader = "Basic " + base64Credentials; + var postData = JSON.stringify( + { + "instanceId": instanceID + }); + // Set request parameters + const options = + { + hostname: baseUrl, + port: 443, + path: path, + method: 'POST', + headers: + { + 'Authorization':authorizationHeader, + 'Content-Type': 'application/json', + // 'Content-Length': postData.length + } + } + // Create request object and send request + console.log("header", options.headers) + console.log("URL", options.hostname, options.path) + console.log("postdata", postData) + axios.post(options.hostname + options.path, postData, { headers: options.headers }) + .then(res => { + console.log(`Status code: ${res.status}`); + const data = res.data; + builderToken = data.tokenName; + console.log(builderToken); + startRealTimeTranscription(); + }) + .catch(error => { + console.error(error); + }); + } + ``` + + A `builderToken` is usable for 5 minutes. After the time has expired, you must generate a new token. + +1. **Start a real-time transcription task** + + To start a real-time transcription task, you: + 1. Create a JSON configuration and pass it in the body of a `POST` request. + 1. Add the `builderToken` as a query parameter to the URL. + 1. Make a `POST` call to . + On success, the method returns a `taskId` that you use to query or stop the task. + + To start a task, in `main.js`, add the following method before `window.onload = () =>`: + + ```javascript + function startRealTimeTranscription() + { + var url = "/v1/projects/" + appId + "/rtsc/speech-to-text/tasks?" + "builderToken=" + builderToken; + var body = JSON.stringify( + { + "audio": + { + "subscribeSource": "AGORARTC", + "agoraRtcConfig": + { + "channelName": channel, + "uid": "1", + "token": token, + "channelType": "LIVE_TYPE", + "subscribeConfig": + { + "subscribeMode": "CHANNEL_MODE" + }, + "maxIdleTime": 60 + } + }, + "config": + { + "features": + [ + "RECOGNIZE" + ], + "recognizeConfig": + { + "language": "ENG", + "model": "Model", + "output": + { + "destinations": + [ + "AgoraRTCDataStream", + "Storage" + ], + "agoraRTCDataStream": + { + "channelName": channel, + "uid": "1", + "token": token + }, + "cloudStorage": + [ + { + "format": "HLS", + "storageConfig": + { + "vendor": 0, + "region": 0, + "bucket":"", + "accessKey":"", + "secretKey":"" + }, + "fileNamePrefix": + [ + "directory1", + "directory2" + ] + } + ] + } + } + } + }); + const options = + { + hostname: baseUrl, + path: url, + method: 'POST', + headers: + { + 'Authorization':authorizationHeader, + 'Content-Type': 'application/json', + 'Content-Length': body.length + } + } + // Create request object and send request + axios.post(options.hostname + options.path, body, { headers: options.headers }) + .then(res => { + console.log(`Status code: ${res.status}`); + const data = res.data; + const status = data.status; + realTimeTranscriptionTaskId = data.taskId; + if (status == "STARTED" || status == "IN_PROGRESS") { + isRealTimeTranscriptionRunning = true; + taskId = data.taskId; + document.getElementById("realTimeTranscription").innerHTML = "Stop real-time transcription"; + console.log("real-time transcription task started"); + } else { + isRealTimeTranscriptionRunning = false; + console.log("real-time transcription status: " + status); + } + }) + .catch(error => { + console.error("Error parsing query call response: " + error); + }); + } + ``` + +1. **Query task status** + + To notify the user of any change to the real-time transcription task, you query the task status using a `GET` request containing the `taskId` and `builderToken`. + + In `main.js`, add the following method before `document.getElementById("join").onclick = async function ()`: + + ```javascript + document.getElementById("realTimeTranscriptionQuery").onclick = async function () + { + var path = "/v1/projects/" + appId + "/rtsc/speech-to-text/tasks/" + realTimeTranscriptionTaskId + "?builderToken=" + builderToken; + // Set request parameters + const options = + { + hostname: baseUrl, + port: 443, + path: path, + method: 'GET', + headers: + { + 'Authorization':authorizationHeader, + 'Content-Type': 'application/json', + } + } + axios.get(options.hostname + options.path, { headers: options.headers }) + .then(res => { + console.log(`Status code: ${res.status}`); + const data = res.data; + const status = data.status; + realTimeTranscriptionTaskId = data.taskId; + if (status == "IN_PROGRESS") { + isRealTimeTranscriptionRunning = true; + document.getElementById("realTimeTranscription").innerHTML = "Stop real-time transcription"; + console.log("real-time transcription is running"); + } else { + isRealTimeTranscriptionRunning = false; + realTimeTranscriptionTaskId = ""; + console.log("real-time transcription status: " + status + ""); + document.getElementById("realTimeTranscription").innerHTML = "Start real-time transcription"; + } + }) + .catch(error => { + console.error(error); + }); + } + ``` + +1. **Stop the task** + + To stop real-time transcription, you send a `DELETE` request with the `appId`, `taskId`, and `builderToken` in the URL. + + In the `main.js`, add the following method before `window.onload = () =>`: + + ```javascript + function stopRealTimeTranscription() + { + var path = "/v1/projects/" + appId + + "/rtsc/speech-to-text/tasks/" + realTimeTranscriptionTaskId + + "?builderToken=" + builderToken; + const options = + { + hostname: baseUrl, + port: 443, + path: path, + method: 'DELETE', + headers: + { + 'Authorization': authorizationHeader, + 'Content-Type': 'application/json', + } + } + // Create request object and send request + axios.post(options) + .then(res => { + console.log(`Status code: ${res.status}`) + if(res.status === 200) { + console.log("Real-time transcription task stopped"); + realTimeTranscriptionTaskId = ""; + document.getElementById("realTimeTranscription").innerHTML = "Start real-time transcription" + isRealTimeTranscriptionRunning = false; + } + console.log(res.data); + }) + .catch(error => { + console.error("Error while stopping the real-time transcription task: " + error) + }); + } + ``` + +1. **Receive and display the text stream** + + When you set one of the `destinations` in the starting configuration as `AgoraRTCDataStream`, the real-time transcription conversion output stream is pushed to your channel. + + To receive this stream and display the text in your , in `main.js`, add the following code after the declarations: + + ```javascript + const EventHandles = + { + // Listen for the onUserJoined to setup the remote view. + onUserJoined: (connection, remoteUid, elapsed) => + { + // Save the remote UID for reuse. + remoteUid = remoteUid; + // Assign the remote UID to the local video container. + remoteVideoContainer.textContent = "Remote user " + remoteUID.toString(); + // Setup remote video to display the remote video. + agoraEngine.setupRemoteVideoEx( + { + sourceType: VideoSourceType.VideoSourceRemote, + uid: remoteUid, + view:remoteVideoContainer, + renderMode: RenderModeType.RenderModeFit, + }, + { + channelId: connection.channelId + }); + }, + onStreamMessage: (connection, remoteUid, streamId, data, length, sentTs) => + { + var text = new TextDecoder().decode(data); + console.log(text); + }, + }; + ``` + diff --git a/shared/video-sdk/develop/real-time-transcription/project-implementation/windows.mdx b/shared/video-sdk/develop/real-time-transcription/project-implementation/windows.mdx new file mode 100644 index 000000000..20c2bfae3 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-implementation/windows.mdx @@ -0,0 +1,493 @@ + + +### Implement the user interface + +To create a simple test interface, add buttons to start and query real-time transcription. To implement + this UI, do the following: + +1. Click **View** > **ToolBox**. + + The **Toolbox** opens. + +1. From **ToolBox**, drag **Button** to the surface of the **Dialog Editor**. + +1. In **Properties**, update the **Caption** field to `Start real-time transcription`. + +1. Repeat the same procedure and add another button where **Caption** is `Query real-time transcription`. + +You see errors in your IDE. This is because the layout refers to methods that you create later. + +### Handle the system logic + +To integrate HTTP and JSON capabilities into your and connect to real-time transcription endpoints: + +1. **Add cJSON library in your project** + + In **Solution Explorer**, right-click **AgoraImplementation**, and then click **Manage NuGet Packages**. The + **NuGet: AgoraImplementation** window opens. Go to the **Browse** tab and type `CppRequests`, then install the + package in your project. + +1. **Import the required classes** + + To use the `CHttpFile` and `cJSON` classes in your project, add the following statements after the last `include` statement in `CAgoraImplementationDlg.cpp`: + + ```cpp + #include + #include + ``` + +1. **Define variables to manage a real-time transcription task** + + In `AgoraImplementationDlg.h`, add the following declarations to the `CAgoraImplementationDlg` class: + + ```cpp + CString baseUrl = L"api.agora.io"; + std::string apiKey = ""; + std::string apiSecret = ""; + CString instanceID = L"Test-1"; + CString authorizationHeader, builderToken; + CString realTimeTranscriptionTaskId; // Returned by the start method + BOOL isRealTimeTranscriptionRunning = false; + std::string Uid = "1"; + CButton* realTimeTranscription; + CButton* queryRealTimeTranscription; + ``` + +1. **Access the buttons programmatically** + + In `AgoraImplementationDlg.cpp`, add the following code at the end of `OnInitDialog`: + + ```cpp + realTimeTranscription = (CButton*)GetDlgItem(IDC_BUTTON3); + realTimeTranscription->MoveWindow(380, 739, 400, 60, TRUE); + queryRealTimeTranscription = (CButton*)GetDlgItem(IDC_BUTTON4); + queryRealTimeTranscription->MoveWindow(380, 666, 400, 60, TRUE); + ``` + +### Manage a real-time transcription task + +Take the following steps to start, stop, and query a real-time transcription task in your . + +1. **Get a `builderToken`** + + To acquire a `builderToken`, you: + + 1. Generate a Base64-encoded credential from the API key and secret associated with your project. + 1. Add this credential to the request header as a parameter named `Authorization`. + 1. In the request body, pass the `instanceID` that you use to identify the instance. + + To acquire a `builderToken`, do the following: + + 1. Convert the secret key and API key to Base64 format: + + 1. In `AgoraImplementationDlg.h`, add the following declaration to `CAgoraImplementationDlg`: + + ```cpp + std::string Encode(const std::string data); + ``` + + 1. In `AgoraImplementationDlg.cpp`, add the following before `setupVideoSDKEngine`: + + ```cpp + std::string CAgoraImplementationDlg::Encode(const std::string data) + { + static constexpr char sEncodingTable[] = + { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', + '4', '5', '6', '7', '8', '9', '+', '/' + }; + size_t in_len = data.size(); + size_t out_len = 4 * ((in_len + 2) / 3); + std::string ret(out_len, '\0'); + size_t i = 0; + char* p = const_cast(ret.c_str()); + if (in_len >= 2) + { + for (i = 0; i < in_len - 2; i += 3) + { + *p++ = sEncodingTable[(data[i] >> 2) & 0x3F]; + *p++ = sEncodingTable[((data[i] & 0x3) << 4) | ((int)(data[i + 1] & 0xF0) >> 4)]; + *p++ = sEncodingTable[((data[i + 1] & 0xF) << 2) | ((int)(data[i + 2] & 0xC0) >> 6)]; + *p++ = sEncodingTable[data[i + 2] & 0x3F]; + } + } + if (i < in_len) + { + *p++ = sEncodingTable[(data[i] >> 2) & 0x3F]; + if (i == (in_len - 1)) + { + *p++ = sEncodingTable[((data[i] & 0x3) << 4)]; + *p++ = '='; + } + else + { + *p++ = sEncodingTable[((data[i] & 0x3) << 4) | ((int)(data[i + 1] & 0xF0) >> 4)]; + *p++ = sEncodingTable[((data[i + 1] & 0xF) << 2)]; + } + *p++ = '='; + } + return ret; + } + ``` + + 1. Retrieve a token from the server: + + 1. In `AgoraImplementationDlg.h`, add the following method to `CAgoraImplementationDlg`: + + ```cpp + void fetchBuilderToken(); + ``` + + 1. In `AgoraImplementationDlg.cpp`, add the following before `setupVideoSDKEngine`: + + ```cpp + void CAgoraImplementationDlg::fetchBuilderToken() + { + CInternetSession session; + std::string temp = "/v1/projects/" + appId + "/rtsc/speech-to-text/builderTokens"; + CString path(temp.c_str()); + CString Url = baseUrl + path; + // Concatenate the key and secret and use base64 encoding + std::string plainCredentials = apiKey + ":" + apiSecret; + std::string base64 = Encode(plainCredentials); + CString CBase64(base64.c_str()); + CHttpConnection* pConnection = NULL; + try + { + pConnection = (CHttpConnection*)session.GetHttpConnection(baseUrl, (INTERNET_PORT)INTERNET_DEFAULT_HTTP_PORT); + if (pConnection) + { + CHttpFile* pHTTPFile = NULL; + CString strHeaders = _T("Content-Type: application/json\r\n"); + authorizationHeader = L"Authorization : Basic "; + authorizationHeader += CBase64; + authorizationHeader += L"\r\n"; + pHTTPFile = pConnection->OpenRequest(CHttpConnection::HTTP_VERB_POST, path); + cJSON* root = cJSON_CreateObject(); + cJSON_AddItemToObject(root, "instanceId", cJSON_CreateString((char*)LPCTSTR(instanceID))); + char* data = NULL; + data = cJSON_Print(root); + strHeaders += authorizationHeader; + if (pHTTPFile) + { + pHTTPFile->AddRequestHeaders(strHeaders); + pHTTPFile->SendRequestEx(strlen(data)); + pHTTPFile->WriteString((LPCTSTR)data); + pHTTPFile->EndRequest(); + DWORD dwRet; + CString strHtml; + char result[1024]; + pHTTPFile->QueryInfoStatusCode(dwRet); + if (dwRet == HTTP_STATUS_OK) + { + pHTTPFile->Read((void*)result, 1024); + cJSON* root = cJSON_Parse(result); + cJSON* success = cJSON_GetObjectItem(root, "tokenName"); + builderToken = success->valuestring; + TRACE(builderToken); + } + else + { + CString errorCode; + pHTTPFile->QueryInfo(HTTP_QUERY_STATUS_TEXT, errorCode); // Status String - eg OK, Not Found + AfxMessageBox(errorCode); + } + pHTTPFile->Close(); + delete pHTTPFile; + } + pConnection->Close(); + delete pConnection; + } + } + catch (CInternetException* e) + { + char strErrorBuf[255]; + e->GetErrorMessage((LPTSTR)strErrorBuf, 255, NULL); + AfxMessageBox((LPCTSTR)strErrorBuf, MB_ICONINFORMATION); + } + session.Close(); + } + ``` + To execute this function at startup, in `AgoraImplementationDlg.cpp`, add the following at the end of `setupVideoSDKEngine`: + + ```cpp + fetchBuilderToken(); + ``` + A `builderToken` is usable for 5 minutes. After the time has expired, you must generate a new token. + +1. **Start a real-time transcription task** + + To start a real-time transcription task, you: + 1. Create a JSON configuration and pass it in the body of a `POST` request. + 1. Add the `builderToken` as a query parameter to the URL. + 1. Make a `POST` call to . + On success, the method returns a `taskId` that you use to query or stop the task. + + To start a task, in **Dialog Editor**, double-click **Start real-time transcription**. **Dialog Editor** automatically creates and opens an event listener for you. Add the following code to the event listener you just created: + + ```cpp + if (isRealTimeTranscriptionRunning) + { + stopRealTimeTranscription(); + return; + } + std::string temp = "/v1/projects/" + appId +"/rtsc/speech-to-text/tasks?" + "builderToken="; + CString path(temp.c_str()); + path += builderToken; + CString Url = baseUrl + path; + // Setup a JSON string to specify a real-time transcription configuration. + cJSON* root = cJSON_CreateObject(); + cJSON* audio = cJSON_CreateObject(); + cJSON* agoraRtcConfig = cJSON_CreateObject(); + cJSON_AddItemToObject(audio, "agoraRtcConfig", agoraRtcConfig); + cJSON_AddItemToObject(audio, "subscribeSource", cJSON_CreateString("AGORARTC")); + cJSON_AddStringToObject(agoraRtcConfig, "channelName", channelName.c_str()); + cJSON_AddStringToObject(agoraRtcConfig, "uid", Uid.c_str()); + cJSON_AddStringToObject(agoraRtcConfig, "token", token.c_str()); + cJSON_AddStringToObject(agoraRtcConfig, "channelType", "LIVE_TYPE"); + cJSON* subscribeConfig = cJSON_CreateObject(); + cJSON_AddStringToObject(subscribeConfig, "subscribeMode", "CHANNEL_MODE"); + cJSON_AddItemToObject(agoraRtcConfig, "subscribeConfig", subscribeConfig); + cJSON_AddItemToObject(agoraRtcConfig, "maxIdleTime", cJSON_CreateInt(60,0)); + cJSON_AddItemToObject(root, "audio", audio); + cJSON* config = cJSON_CreateObject(); + cJSON* features = cJSON_CreateArray(); + cJSON_AddItemToArray(features, cJSON_CreateString("RECOGNIZE")); + cJSON_AddItemToObject(config, "features", features); + cJSON* recognizeConfig = cJSON_CreateObject(); + cJSON_AddStringToObject(recognizeConfig, "language", "ENG"); + cJSON_AddStringToObject(recognizeConfig, "model", "Model"); + cJSON* output = cJSON_CreateObject(); + cJSON* destinations = cJSON_CreateArray(); + cJSON_AddItemToArray(destinations, cJSON_CreateString("AgoraRTCDataStream")); + cJSON_AddItemToArray(destinations, cJSON_CreateString("Storage")); + cJSON_AddItemToObject(output, "destinations", destinations); + cJSON* agoraRTCDataStream = cJSON_CreateObject(); + cJSON_AddStringToObject(agoraRTCDataStream, "channelName", channelName.c_str()); + cJSON_AddStringToObject(agoraRTCDataStream, "uid", Uid.c_str()); + cJSON_AddStringToObject(agoraRTCDataStream, "token", token.c_str()); + cJSON_AddItemToObject(output, "agoraRTCDataStream", agoraRTCDataStream); + cJSON* cloudStorage = cJSON_CreateArray(); + cJSON* unknwon = cJSON_CreateObject(); + cJSON_AddStringToObject(unknwon, "format", "HLS"); + cJSON* storageConfig = cJSON_CreateObject(); + cJSON_AddItemToObject(storageConfig, "vendor", cJSON_CreateInt(0, 0)); + cJSON_AddItemToObject(storageConfig, "region", cJSON_CreateInt(0, 0)); + cJSON_AddStringToObject(storageConfig, "bucket", "YourBucket"); + cJSON_AddStringToObject(storageConfig, "accessKey", "< YourOssAccessKey >"); + cJSON_AddStringToObject(storageConfig, "secretKey", "< YourOssSecretKey >"); + cJSON_AddItemToObject(unknwon, "storageConfig", storageConfig); + cJSON* fileNamePrefix = cJSON_CreateArray(); + cJSON_AddItemToArray(fileNamePrefix, cJSON_CreateString("directory1")); + cJSON_AddItemToArray(fileNamePrefix, cJSON_CreateString("directory2")); + cJSON_AddItemToObject(unknwon, "fileNamePrefix", fileNamePrefix); + cJSON_AddItemToObject(cloudStorage, "", unknwon); + cJSON_AddItemToObject(output, "cloudStorage", cloudStorage); + cJSON_AddItemToObject(recognizeConfig, "output", output); + cJSON_AddItemToObject(config, "recognizeConfig", recognizeConfig); + cJSON_AddItemToObject(root, "config", config); + char* postData = NULL; + postData = cJSON_Print(root); + CInternetSession session; + CHttpConnection* pConnection = NULL; + try + { + pConnection = (CHttpConnection*)session.GetHttpConnection(baseUrl, (INTERNET_PORT)INTERNET_DEFAULT_HTTP_PORT); + if (pConnection) + { + CHttpFile* pHTTPFile = NULL; + pHTTPFile = pConnection->OpenRequest(CHttpConnection::HTTP_VERB_POST, path); + CString strHeaders = _T("Content-Type: application/json\r\n"); + strHeaders += authorizationHeader; + if (pHTTPFile) + { + pHTTPFile->AddRequestHeaders(strHeaders); + pHTTPFile->SendRequestEx(strlen(postData)); + pHTTPFile->WriteString((LPCTSTR)postData); + pHTTPFile->EndRequest(); + DWORD dwRet; + char result[1024]; + pHTTPFile->QueryInfoStatusCode(dwRet); + if (dwRet == HTTP_STATUS_OK) + { + pHTTPFile->Read((void*)result, 1024); + cJSON* root = cJSON_Parse(result); + cJSON* success = cJSON_GetObjectItem(root, "taskId"); + realTimeTranscriptionTaskId = success->valuestring; + isRealTimeTranscriptionRunning = true; + realTimeTranscription->SetWindowText(L"Stop real-time transcription"); + AfxMessageBox(L"Real-time transcription service has been started successfully"); + } + else + { + CString errorCode; + pHTTPFile->QueryInfo(HTTP_QUERY_STATUS_TEXT, errorCode); // Status String - eg OK, Not Found + AfxMessageBox(errorCode); + } + pHTTPFile->Close(); + delete pHTTPFile; + } + pConnection->Close(); + delete pConnection; + } + } + catch (CInternetException* e) + { + char strErrorBuf[255]; + e->GetErrorMessage((LPTSTR)strErrorBuf, 255, NULL); + AfxMessageBox((LPCTSTR)strErrorBuf, MB_ICONINFORMATION); + } + session.Close(); + ``` + +1. **Query task status** + + To notify the user of any change to the real-time transcription task, you query the task status using a `GET` request containing the `taskId` and `builderToken`. To implement this workflow, in **Dialog Editor**, double-click **Query real-time transcription**. **Dialog Editor** automatically creates and opens an event listener for you. Add the following code to the event listener you just created: + + ```cpp + std::string temp = "/v1/projects/" + appId + +"/rtsc/speech-to-text/tasks/"; + CString path(temp.c_str()); + path += realTimeTranscriptionTaskId + "?builderToken=" + builderToken; + CInternetSession session; + CHttpConnection* pConnection = NULL; + try + { + pConnection = (CHttpConnection*)session.GetHttpConnection(baseUrl, (INTERNET_PORT)INTERNET_DEFAULT_HTTP_PORT); + if (pConnection) + { + CHttpFile* pHTTPFile = NULL; + pHTTPFile = pConnection->OpenRequest(CHttpConnection::HTTP_VERB_GET, path); + CString strHeaders = _T("Content-Type: application/json\r\n"); + strHeaders += authorizationHeader; + if (pHTTPFile) + { + pHTTPFile->AddRequestHeaders(strHeaders); + pHTTPFile->SendRequest(); + DWORD dwRet; + char result[1024]; + pHTTPFile->QueryInfoStatusCode(dwRet); + if (dwRet == HTTP_STATUS_OK) + { + pHTTPFile->Read((void*)result, 1024); + cJSON* root = cJSON_Parse(result); + cJSON* status = cJSON_GetObjectItem(root, "status"); + TRACE(status->valuestring); + CString CStatus = L"Current status: "; + CStatus += status->valuestring; + AfxMessageBox(CStatus); + } + else + { + CString errorCode; + pHTTPFile->QueryInfo(HTTP_QUERY_STATUS_TEXT, errorCode); // Status String - eg OK, Not Found + AfxMessageBox(errorCode); + } + pHTTPFile->Close(); + delete pHTTPFile; + } + pConnection->Close(); + delete pConnection; + } + } + catch (CInternetException* e) + { + char strErrorBuf[255]; + e->GetErrorMessage((LPTSTR)strErrorBuf, 255, NULL); + AfxMessageBox((LPCTSTR)strErrorBuf, MB_ICONINFORMATION); + } + session.Close(); + ``` + +1. **Stop the task** + + To stop real-time transcription, you send a `DELETE` request with the `appId`, `taskId`, and `builderToken` in the URL. To implement this logic, do the following: + + 1. In `AgoraImplementationDlg.h`, add the following to `CAgoraImplementationDlg`: + + ```cpp + void stopRealTimeTranscription(); + ``` + + 2. In `AgoraImplementationDlg.cpp`, add the following method before `setupVideoSDKEngine`: + + ```cpp + void CAgoraImplementationDlg::stopRealTimeTranscription() + { + std::string temp = "/v1/projects/" + appId + +"/rtsc/speech-to-text/tasks/"; + CString path(temp.c_str()); + path += realTimeTranscriptionTaskId + "?builderToken=" + builderToken; + CInternetSession session; + CHttpConnection* pConnection = NULL; + try + { + pConnection = (CHttpConnection*)session.GetHttpConnection(baseUrl, (INTERNET_PORT)INTERNET_DEFAULT_HTTP_PORT); + if (pConnection) + { + CHttpFile* pHTTPFile = NULL; + pHTTPFile = pConnection->OpenRequest(CHttpConnection::HTTP_VERB_DELETE, path); + CString strHeaders = _T("Content-Type: application/json\r\n"); + strHeaders += authorizationHeader; + if (pHTTPFile) + { + pHTTPFile->AddRequestHeaders(strHeaders); + pHTTPFile->SendRequest(); + DWORD dwRet; + char result[1024]; + pHTTPFile->QueryInfoStatusCode(dwRet); + if (dwRet == HTTP_STATUS_OK) + { + AfxMessageBox(L"Real-time transcription task stopped"); + isRealTimeTranscriptionRunning = false; + realTimeTranscriptionTaskId = ""; + realTimeTranscription->SetWindowText(L"Start real-time transcription"); + } + else + { + CString errorCode; + pHTTPFile->QueryInfo(HTTP_QUERY_STATUS_TEXT, errorCode); // Status String - eg OK, Not Found + AfxMessageBox(errorCode); + } + pHTTPFile->Close(); + delete pHTTPFile; + } + pConnection->Close(); + delete pConnection; + } + } + catch (CInternetException* e) + { + char strErrorBuf[255]; + e->GetErrorMessage((LPTSTR)strErrorBuf, 255, NULL); + AfxMessageBox((LPCTSTR)strErrorBuf, MB_ICONINFORMATION); + } + session.Close(); + } + ``` + +1. **Receive and display the text stream** + + When you set one of the `destinations` in the starting configuration as `AgoraRTCDataStream`, the real-time transcription conversion output stream is pushed to your channel. + + To receive this stream and display the text in your , do the following: + + 1. Add the `onStreamMessage` callback to `AgoraEventHandler`. In `AgoraImplementationDlg.h`, add the following to `AgoraEventHandler`: + + ```cpp + virtual void onStreamMessage(uid_t userId, int streamId, const char* data, size_t length, uint64_t sentTs); + ``` + + 2. Display the text stream in the debugger **Output** window. In `AgoraImplementationDlg.cpp`, add the following before `setupVideoSDKEngine`: + + ```cpp + void AgoraEventHandler::onStreamMessage(uid_t userId, int streamId, const char* data, size_t length, uint64_t sentTs) + { + TRACE(data); + } + ``` + \ No newline at end of file diff --git a/shared/video-sdk/develop/real-time-transcription/project-setup/android.mdx b/shared/video-sdk/develop/real-time-transcription/project-setup/android.mdx new file mode 100644 index 000000000..c56ca9b9b --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-setup/android.mdx @@ -0,0 +1,3 @@ + + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-setup/electron.mdx b/shared/video-sdk/develop/real-time-transcription/project-setup/electron.mdx new file mode 100644 index 000000000..826e1e76d --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-setup/electron.mdx @@ -0,0 +1,3 @@ + + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-setup/flutter.mdx b/shared/video-sdk/develop/real-time-transcription/project-setup/flutter.mdx new file mode 100644 index 000000000..f2141600a --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-setup/flutter.mdx @@ -0,0 +1,3 @@ + + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-setup/index.mdx b/shared/video-sdk/develop/real-time-transcription/project-setup/index.mdx new file mode 100644 index 000000000..fc7763282 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-setup/index.mdx @@ -0,0 +1,17 @@ +import Android from './android.mdx'; +import Ios from './ios.mdx'; +import Web from './web.mdx'; +import Unity from './unity.mdx'; +import ReactNative from './react-native.mdx'; +import Electron from './electron.mdx'; +import Flutter from './flutter.mdx'; +import MacOS from './macos.mdx'; + + + + + + + + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-setup/ios.mdx b/shared/video-sdk/develop/real-time-transcription/project-setup/ios.mdx new file mode 100644 index 000000000..52ad025f4 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-setup/ios.mdx @@ -0,0 +1,3 @@ + + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-setup/macos.mdx b/shared/video-sdk/develop/real-time-transcription/project-setup/macos.mdx new file mode 100644 index 000000000..ad32ac99e --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-setup/macos.mdx @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/shared/video-sdk/develop/real-time-transcription/project-setup/react-native.mdx b/shared/video-sdk/develop/real-time-transcription/project-setup/react-native.mdx new file mode 100644 index 000000000..25a8fca94 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-setup/react-native.mdx @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/shared/video-sdk/develop/real-time-transcription/project-setup/swift.mdx b/shared/video-sdk/develop/real-time-transcription/project-setup/swift.mdx new file mode 100644 index 000000000..e69de29bb diff --git a/shared/video-sdk/develop/real-time-transcription/project-setup/unity.mdx b/shared/video-sdk/develop/real-time-transcription/project-setup/unity.mdx new file mode 100644 index 000000000..72de34065 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-setup/unity.mdx @@ -0,0 +1,3 @@ + + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-setup/web.mdx b/shared/video-sdk/develop/real-time-transcription/project-setup/web.mdx new file mode 100644 index 000000000..8f737f379 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-setup/web.mdx @@ -0,0 +1,3 @@ + + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-test/android.mdx b/shared/video-sdk/develop/real-time-transcription/project-test/android.mdx new file mode 100644 index 000000000..b6f19b853 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-test/android.mdx @@ -0,0 +1,31 @@ + + +3. In Android Studio, open `app/java/com.example./MainActivity` and update `appId`, `channelName`, and `token` with the values from . Make sure that the `uid` is set to a non-zero integer. + +4. In , click the account name in the top right corner, and select **RESTful API** from the drop-down list. If you have not added a secret already, do so now and download it. Update `apiKey` and `aipSecret` in `MainActivity` with these values. + +5. Connect an Android device to your development device. + +6. In Android Studio, click **Run app**. A moment later, you see the project installed on your device. + + If this is the first time you run your , grant camera and microphone permissions. + + +7. Select a user role. Press **Join** to connect to the same channel as your web demo. + + + +7. Press **Join** to connect to the same channel as your web demo. + + +8. Press **Start real-time transcription**. You see notifications confirming receipt of a builder token and starting of a real-time transcription task. + +9. Speak into the microphone. You see spoken words displayed in your as text. + +10. Press **Query real-time transcription**. You see a message informing you of the current status of the task. + +11. Press **Stop real-time transcription**. The real-time transcription task stops and a confirmation message is displayed. + +12. Check the storage location you specified in the starting configuration. You see a `WebVTT` file containing real-time transcription data. + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-test/electron.mdx b/shared/video-sdk/develop/real-time-transcription/project-test/electron.mdx new file mode 100644 index 000000000..2b1c5a0ae --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-test/electron.mdx @@ -0,0 +1,31 @@ + + +3. In **preload.js**, update `appID`, `channel`, and `token` with your values. Make sure that the `Uid` is set to a non-zero integer. + +3. Run the + + Execute the following command in the terminal: + ```bash + npm start + ``` + You see your opens a window named **Get started with **. + + +7. Select a user role. Press **Join** to connect to the same channel as your web demo. + + + +7. Press **Join** to connect to the same channel as your web demo. + + +8. Press **Start real-time transcription**. You see notifications in **Console** confirming receipt of a builder token and starting of a real-time transcription task. + +9. Speak into the microphone. You see spoken words displayed in your as text. + +10. Press **Query real-time transcription**. You see a message in **Console** informing you of the current status of the task. + +11. Press **Stop real-time transcription**. The real-time transcription task stops and a confirmation message is displayed in **Console**. + +12. Check the storage location you specified in the JSON schema. You see a `WebVTT` file containing real-time transcription data. + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-test/flutter.mdx b/shared/video-sdk/develop/real-time-transcription/project-test/flutter.mdx new file mode 100644 index 000000000..0b8315405 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-test/flutter.mdx @@ -0,0 +1,31 @@ + + +3. In Android Studio, open `/lib/main.dart` and update `appId`, `channelName`, and `token` with the values from . Make sure that the `uid` is set to a non-zero integer. + +4. In , click the account name in the top right corner, and select **RESTful API** from the drop-down list. If you have not added a secret already, do so now and download it. Update `apiKey` and `aipSecret` in declarations with these values. + +5. Connect a test device to your development device. + +6. In your IDE, click **Run app**. A moment later, you see the project installed on your device. + + If this is the first time you run your , grant camera and microphone permissions. + + +7. Select a user role. Press **Join** to connect to the same channel as your web demo. + + + +7. Press **Join** to connect to the same channel as your web demo. + + +8. Press **Start real-time transcription**. You see notifications confirming receipt of a builder token and starting of a real-time transcription task. + +1. Speak into the microphone. You see spoken words displayed in your as text. + +1. Press **Query real-time transcription**. You see a message informing you of the current status of the task. + +1. Press **Stop real-time transcription**. The real-time transcription task stops and a confirmation message is displayed. + +1. Check the storage location you specified in the starting configuration. You see a `WebVTT` file containing real-time transcription data. + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-test/index.mdx b/shared/video-sdk/develop/real-time-transcription/project-test/index.mdx new file mode 100644 index 000000000..4fc389223 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-test/index.mdx @@ -0,0 +1,19 @@ +import Android from './android.mdx'; +import Ios from './ios.mdx'; +import Web from './web.mdx'; +import Unity from './unity.mdx'; +import ReactNative from './react-native.mdx'; +import Electron from './electron.mdx'; +import Flutter from './flutter.mdx'; +import MacOS from './macos.mdx'; +import Windows from './windows.mdx'; + + + + + + + + + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-test/ios.mdx b/shared/video-sdk/develop/real-time-transcription/project-test/ios.mdx new file mode 100644 index 000000000..f21f0f8b6 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-test/ios.mdx @@ -0,0 +1,7 @@ +import Source from './swift.mdx'; + + + + + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-test/macos.mdx b/shared/video-sdk/develop/real-time-transcription/project-test/macos.mdx new file mode 100644 index 000000000..38ff7300d --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-test/macos.mdx @@ -0,0 +1,7 @@ +import Source from './swift.mdx'; + + + + + + \ No newline at end of file diff --git a/shared/video-sdk/develop/real-time-transcription/project-test/react-native.mdx b/shared/video-sdk/develop/real-time-transcription/project-test/react-native.mdx new file mode 100644 index 000000000..8835909e7 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-test/react-native.mdx @@ -0,0 +1,29 @@ + + +3. In `App.tsx`, update `appID`, `channel`, and `token` with your values. +4. Run the on either: + + * An Android physical device: + + 1. Enable the **Developer** options on your Android device, and then connect it to your computer using a USB + cable. + 1. Run `npx react-native run-android` in the project root directory. + + * An iOS physical device: + + 1. In Xcode, open the `ProjectName/ios/ProjectName.xcworkspace`. + 1. Connect your iOS device to your Mac using a USB cable. + 1. Click **Build and run**. + +6. To connect to a channel, click **Join**. +8. Press **Start real-time transcription**. You see notifications in **Console** confirming receipt of a builder token and starting of a real-time transcription task. + +9. Speak into the microphone. You see spoken words displayed in your as text. + +10. Press **Query real-time transcription**. You see a message in **Console** informing you of the current status of the task. + +11. Press **Stop real-time transcription**. The real-time transcription task stops and a confirmation message is displayed in **Console**. + +12. Check the storage location you specified in the JSON schema. You see a `WebVTT` file containing real-time transcription data. + + \ No newline at end of file diff --git a/shared/video-sdk/develop/real-time-transcription/project-test/swift.mdx b/shared/video-sdk/develop/real-time-transcription/project-test/swift.mdx new file mode 100644 index 000000000..e115746bd --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-test/swift.mdx @@ -0,0 +1,28 @@ + +3. In the `ViewController`, update `appID`, `channelName`, and `token` with the values from . Make sure that the `uid` is set to a non-zero integer. + +4. In , click the account name in the top right corner, and select **RESTful API** from the drop-down list. If you have not added a secret already, do so now and download it. Update `apiKey` and `aipSecret` in `ViewController` with these values. + +5. Run your . + + If this is the first time you run the project, grant microphone and camera access to your . + + If you use an iOS simulator, you see the remote video only. You cannot see the local video stream because of Apple simulator hardware restrictions. + + +6. Select **Broadcaster** mode and press **Join** to connect to the same channel as your web demo. + + + +6. Press **Join** to connect to the same channel as your web demo. + + +7. Press **Start real-time transcription**. You see notifications confirming receipt of a builder token and starting of a real-time transcription task. + +8. Speak into the microphone. You see spoken words displayed in your as text. + +9. Press **Query real-time transcription**. You see a message informing you of the current status of the task. + +10. Press **Stop real-time transcription**. The real-time transcription task stops and a confirmation message is displayed. + +11. Check the storage location you specified in the starting configuration. You see a `WebVTT` file containing real-time transcription data. \ No newline at end of file diff --git a/shared/video-sdk/develop/real-time-transcription/project-test/unity.mdx b/shared/video-sdk/develop/real-time-transcription/project-test/unity.mdx new file mode 100644 index 000000000..5add810fd --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-test/unity.mdx @@ -0,0 +1,27 @@ + + +3. In **Unity Editor**, in your script file, update `_appID`, `_channelName`, and `_token` with the values for your temporary token. Make sure that the `Uid` is set to a non-zero integer. + +4. In , click the account name in the top right corner, and select **RESTful API** from the drop-down list. If you have not added a secret already, do so now and download it. Update `apiKey` and `aipSecret` in `NewBehaviourScript` with these values. + +5. In **Unity Editor**, click **Play**. A moment later you see the running on your development device. + + +6. Select a user role. Press **Join** to connect to the same channel as your web demo. + + + +6. Press **Join** to connect to the same channel as your web demo. + + +7. Press **Start real-time transcription**. You see notifications in the **Debug Console** confirming receipt of a builder token and starting of a real-time transcription task. + +8. Speak into the microphone. You see spoken words displayed in the **Debug Console** as text. + +9. Press **Query real-time transcription**. You see a message informing you of the current status of the task. + +10. Press **Start real-time transcription** again. The real-time transcription task stops and a confirmation message is displayed in the **Debug Console**. + +11. Check the storage location you specified in the starting configuration. You see a `WebVTT` file containing real-time transcription data. + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-test/web.mdx b/shared/video-sdk/develop/real-time-transcription/project-test/web.mdx new file mode 100644 index 000000000..bd708695b --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-test/web.mdx @@ -0,0 +1,33 @@ + + +3. In **main.js**, update `appID`, `channel`, and `token` with your values. Make sure that the `Uid` is set to a non-zero integer. + +3. Update the `apiKey` and `apiSecret` with your values from . + +3. Run the + + Execute the following command in the terminal: + ```bash + npm run dev + ``` + You see your opens a window named **Get started with **. + + +6. Select a user role. Press **Join** to connect to the same channel as your web demo. + + + +6. Press **Join** to connect to the same channel as your web demo. + + +7. Press **Start real-time transcription**. You see notifications in **Debug Console** confirming receipt of a builder token and starting of a real-time transcription task. + +7. Speak into the microphone. You see spoken words displayed in your as text. + +7. Press **Query real-time transcription**. You see a message in **Debug Console** informing you of the current status of the task. + +7. Press **Stop real-time transcription**. The real-time transcription task stops and a confirmation message is displayed in **Debug Console**. + +7. Check the storage location you specified in the JSON schema. You see a `WebVTT` file containing real-time transcription data. + + diff --git a/shared/video-sdk/develop/real-time-transcription/project-test/windows.mdx b/shared/video-sdk/develop/real-time-transcription/project-test/windows.mdx new file mode 100644 index 000000000..03db294c9 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/project-test/windows.mdx @@ -0,0 +1,27 @@ + + +3. In `CAgoraImplementationDlg.h`, update `appId`, `channelName`, and `token` with the values for your temporary token. + +1. In Visual Studio, click **Local Windows Debugger**. A moment later you see the project running on your development device. + + If this is the first time you run the project, you need to grant microphone and camera access to your . + + +7. Select a user role. Press **Join** to connect to the same channel as your web demo. + + + +7. Press **Join** to connect to the same channel as your web demo. + + +8. Press **Start real-time transcription**. You see a token in the Debugger **Output** window. + +9. Speak into the microphone. You see spoken words displayed in the Debugger **Output** window as text. + +10. Press **Query real-time transcription**. You see a message informing you of the current status of the task. + +11. Press **Stop real-time transcription**. The real-time transcription task stops and a confirmation message is displayed. + +12. Check the storage location you specified in the starting configuration. You see a `WebVTT` file containing real-time transcription data. + + diff --git a/shared/video-sdk/develop/real-time-transcription/reference/android.mdx b/shared/video-sdk/develop/real-time-transcription/reference/android.mdx new file mode 100644 index 000000000..9811e2296 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/reference/android.mdx @@ -0,0 +1,7 @@ + + +### API reference + +- onStreamMessage + + diff --git a/shared/video-sdk/develop/real-time-transcription/reference/electron.mdx b/shared/video-sdk/develop/real-time-transcription/reference/electron.mdx new file mode 100644 index 000000000..93f045165 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/reference/electron.mdx @@ -0,0 +1,7 @@ + + +### API reference + +- onStreamMessage + + diff --git a/shared/video-sdk/develop/real-time-transcription/reference/flutter.mdx b/shared/video-sdk/develop/real-time-transcription/reference/flutter.mdx new file mode 100644 index 000000000..567d8df5b --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/reference/flutter.mdx @@ -0,0 +1,8 @@ + + +### API reference + +- onStreamMessage + + + diff --git a/shared/video-sdk/develop/real-time-transcription/reference/index.mdx b/shared/video-sdk/develop/real-time-transcription/reference/index.mdx new file mode 100644 index 000000000..250435457 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/reference/index.mdx @@ -0,0 +1,24 @@ +import Android from './android.mdx'; +import Ios from './ios.mdx'; +import Web from './web.mdx'; +import Unity from './unity.mdx'; +import ReactNative from './react-native.mdx'; +import Electron from './electron.mdx'; +import Flutter from './flutter.mdx'; +import MacOS from './macos.mdx'; +import Windows from './windows.mdx'; + + +- Android: onStreamMessage +- Electron: onStreamMessage +- Flutter: onStreamMessage +- iOS: receiveStreamMessageFromUid +- macOS:receiveStreamMessageFromUid +- Unity: onStreamMessage +- Windows: onStreamMessage + + + + + + diff --git a/shared/video-sdk/develop/real-time-transcription/reference/ios.mdx b/shared/video-sdk/develop/real-time-transcription/reference/ios.mdx new file mode 100644 index 000000000..8da844e91 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/reference/ios.mdx @@ -0,0 +1,7 @@ + + +### API reference + +- receiveStreamMessageFromUid + + diff --git a/shared/video-sdk/develop/real-time-transcription/reference/macos.mdx b/shared/video-sdk/develop/real-time-transcription/reference/macos.mdx new file mode 100644 index 000000000..7581aae4f --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/reference/macos.mdx @@ -0,0 +1,7 @@ + + +### API reference + +- receiveStreamMessageFromUid + + \ No newline at end of file diff --git a/shared/video-sdk/develop/real-time-transcription/reference/react-native.mdx b/shared/video-sdk/develop/real-time-transcription/reference/react-native.mdx new file mode 100644 index 000000000..25a8fca94 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/reference/react-native.mdx @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/shared/video-sdk/develop/real-time-transcription/reference/swift.mdx b/shared/video-sdk/develop/real-time-transcription/reference/swift.mdx new file mode 100644 index 000000000..e69de29bb diff --git a/shared/video-sdk/develop/real-time-transcription/reference/unity.mdx b/shared/video-sdk/develop/real-time-transcription/reference/unity.mdx new file mode 100644 index 000000000..a4029a477 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/reference/unity.mdx @@ -0,0 +1,7 @@ + + +### API reference + +- onStreamMessage + + diff --git a/shared/video-sdk/develop/real-time-transcription/reference/web.mdx b/shared/video-sdk/develop/real-time-transcription/reference/web.mdx new file mode 100644 index 000000000..8f737f379 --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/reference/web.mdx @@ -0,0 +1,3 @@ + + + diff --git a/shared/video-sdk/develop/real-time-transcription/reference/windows.mdx b/shared/video-sdk/develop/real-time-transcription/reference/windows.mdx new file mode 100644 index 000000000..92ba4f42e --- /dev/null +++ b/shared/video-sdk/develop/real-time-transcription/reference/windows.mdx @@ -0,0 +1,7 @@ + + +### API reference + +- onStreamMessage + + diff --git a/shared/video-sdk/develop/video-compositor/project-implementation/index.mdx b/shared/video-sdk/develop/video-compositor/project-implementation/index.mdx new file mode 100644 index 000000000..0e8c2a870 --- /dev/null +++ b/shared/video-sdk/develop/video-compositor/project-implementation/index.mdx @@ -0,0 +1,3 @@ +import Web from './web.mdx'; + + \ No newline at end of file diff --git a/shared/video-sdk/develop/video-compositor/project-implementation/web.mdx b/shared/video-sdk/develop/video-compositor/project-implementation/web.mdx new file mode 100644 index 000000000..c61cfeaea --- /dev/null +++ b/shared/video-sdk/develop/video-compositor/project-implementation/web.mdx @@ -0,0 +1,130 @@ + +1. **Import the required modules** + + Use any of the following methods to import the Video Compositor extension. + + - Add the following code to the `main.js` file: + + ```javascript + import VideoCompositingExtension from "agora-extension-video-compositor"; + ``` + + - Import via Script tag in the `index.html` file: + + ```html + + ``` + +1. **Create and register the extensions** + + Create the `VideoCompositingExtension` object and call `AgoraRTC.registerExtensionsregister` to register the extension. Then, create a `VideoTrackCompositor` object. + + ``` javascript + // Create VideoCompositingExtension and VirtualBackgroundExtension object + const extension = new VideoCompositingExtension(); + const vbExtension = new VirtualBackgroundExtension(); + // Register extensions + AgoraRTC.registerExtensions([extension, vbExtension]); + // Create VideoTrackCompositor object + let compositor = extension.createProcessor(); + let vbProcessor = null; + ``` + +1. **Create the tracks** + + Call the `createScreenVideoTrack`, `createCameraVideoTrack`, and `createCustomVideoTrack` methods respectively to create three video tracks: + + ``` javascript + // Create a screen sharing video track + screenShareTrack = await AgoraRTC.createScreenVideoTrack({encoderConfig: {frameRate: 15}}); + + // Create a source video track using video captured by a camera + sourceVideoTrack1 = await AgoraRTC.createCameraVideoTrack({cameraId: videoSelect.value, encoderConfig: '720p_1'}) + + // Create a source video track using a local video file + const width = 1280, height = 720; + const videoElement = await createVideoElement(width, height, './assets/loop-video.mp4'); + const mediaStream = videoElement.captureStream(); + const msTrack = mediaStream.getVideoTracks()[0]; + sourceVideoTrack2 = AgoraRTC.createCustomVideoTrack({ mediaStreamTrack: msTrack }); + ``` + +1. **Create the input layers** + + Create the input layers for the image and video tracks in order from bottom to top, with layers created later covering the top. In the code below, the screen share image is at the bottom and the image from source video track 2 is at the top: + + ``` javascript + // Create the input layer for the screen share video track + const screenShareEndpoint = processor.createInputEndpoint({x: 0, y: 0, width: 1280, height: 720, fit: 'cover'}); + // Create an input layer for images + compositor.addImage('./assets/city.jpg', {x: 960, y: 0, width: 320, height: 180, fit: 'cover'}) + compositor.addImage('./assets/space.jpg', {x: 0, y: 540, width: 320, height: 180, fit: 'cover'}) + // Create input layers for source video tracks 1 and 2 + const endpoint1 = compositor.createInputEndpoint({x: 0, y: 0, width: 320, height: 180, fit: 'cover'}); + const endpoint2 = compositor.createInputEndpoint({x: 960, y: 540, width: 320, height: 180, fit: 'cover'}); + // Set the virtual background for source video track 1 + if (!vbProcessor) { + vbProcessor = vbExtension.createProcessor(); + await vbProcessor.init("./assets/wasms"); + vbProcessor.enable(); + vbProcessor.setOptions({type: 'none'}); + } + // Connect the pipeline between the video input layer and the video track + screenShareTrack.pipe(screenShareEndpoint).pipe(screenShareTrack.processorDestination); + sourceVideoTrack1.pipe(vbProcessor).pipe(endpoint1).pipe(sourceVideoTrack1.processorDestination); + sourceVideoTrack2.pipe(endpoint2).pipe(sourceVideoTrack2.processorDestination); + ``` + +1. **Merge all the layers** + + Merge all input layers and inject the merged video into the local video track: + + ``` javascript + const canvas = document.createElement('canvas'); + canvas.getContext('2d'); + // Create a local video track + localTracks.videoTrack = AgoraRTC.createCustomVideoTrack({ mediaStreamTrack: canvas.captureStream().getVideoTracks()[0]}); + + // Set up image options + compositor.setOutputOptions(1280, 720, 15); + // Start merging + await compositor.start(); + // Inject the merged video into the local video track + localTracks.videoTrack.pipe(compositor).pipe(localTracks.videoTrack.processorDestination); + ``` + +1. **Publish the merged video**: + + To play and publish a local video track: + + ```javascript + // Play local video track + localTracks.videoTrack.play("local-player"); + + // Publish local audio and video tracks + localTracks.audioTrack = localTracks.audioTrack || await AgoraRTC.createMicrophoneAudioTrack(); + await client.publish(Object.values(localTracks)); + ``` + +1. **Clean up upon leaving the channel** + + When you need to leave the channel, call `unpipe` to disconnect the pipeline of the compositor and all video tracks, and stop all audio and video tracks, otherwise an error may occur when you join the channel again: + + ```javascript + async function leave() { + await client.leave(); + localTracks.audioTrack?.close(); + localTracks.videoTrack?.unpipe(); + localTracks.videoTrack?.close(); + compositor?.unpipe(); + vbProcessor?.unpipe(); + sourceVideoTrack1?.unpipe(); + sourceVideoTrack1?.close(); + sourceVideoTrack2?.unpipe(); + sourceVideoTrack2?.close(); + screenShareTrack?.unpipe(); + screenShareTrack.close(); + } + ``` + + diff --git a/shared/video-sdk/develop/video-compositor/project-setup/index.mdx b/shared/video-sdk/develop/video-compositor/project-setup/index.mdx new file mode 100644 index 000000000..0e8c2a870 --- /dev/null +++ b/shared/video-sdk/develop/video-compositor/project-setup/index.mdx @@ -0,0 +1,3 @@ +import Web from './web.mdx'; + + \ No newline at end of file diff --git a/shared/video-sdk/develop/video-compositor/project-setup/web.mdx b/shared/video-sdk/develop/video-compositor/project-setup/web.mdx new file mode 100644 index 000000000..3be7fe1bd --- /dev/null +++ b/shared/video-sdk/develop/video-compositor/project-setup/web.mdx @@ -0,0 +1,20 @@ + +In a remote meeting, in addition to showing PPT and portrait images, the speaker may also need to use pictures, video files, and other materials to assist with the presentation. The audience may want to see the following effects: + +![Video Compositor](/images/video-calling/video-composite-example.png) + +In this scenario, we need to composite the following onto one video track: + +- Screen sharing video track. +- Two local images. +- Source video track 1: Created from a video stream captured by a camera, and the background of this track is removed using the Virtual Background extension. +- Source video track 2: Created from a local video file. + +In order to create the environment necessary to implement this extension in your , open the [](../get-started/get-started-sdk) project you created previously. + +1. Run the following command to install the Video Compositor extension: + + ```bash + npm install agora-extension-video-compositor + ``` + diff --git a/shared/video-sdk/develop/video-compositor/project-test/index.mdx b/shared/video-sdk/develop/video-compositor/project-test/index.mdx new file mode 100644 index 000000000..0e8c2a870 --- /dev/null +++ b/shared/video-sdk/develop/video-compositor/project-test/index.mdx @@ -0,0 +1,3 @@ +import Web from './web.mdx'; + + \ No newline at end of file diff --git a/shared/video-sdk/develop/video-compositor/project-test/web.mdx b/shared/video-sdk/develop/video-compositor/project-test/web.mdx new file mode 100644 index 000000000..89d22982a --- /dev/null +++ b/shared/video-sdk/develop/video-compositor/project-test/web.mdx @@ -0,0 +1,27 @@ + + +3. In _main.js_, update `appID`, `channel`, and `token` with your values. + +4. Start the dev server + + Execute the following command in the terminal: + + ```bash + npm run dev + ``` + Use the url displayed in the terminal to open the in your browser. + + +5. To join as a host, select **Host** and click **Join**. + + + + +5. To connect to a channel, click **Join**. + + +6. Grant microphone and camera access to your . + + You see your starts the proxy service and magically connects to the which was not possible in a restricted network environment. + + diff --git a/shared/video-sdk/develop/video-compositor/reference/index.mdx b/shared/video-sdk/develop/video-compositor/reference/index.mdx new file mode 100644 index 000000000..0e8c2a870 --- /dev/null +++ b/shared/video-sdk/develop/video-compositor/reference/index.mdx @@ -0,0 +1,3 @@ +import Web from './web.mdx'; + + \ No newline at end of file diff --git a/shared/video-sdk/develop/video-compositor/reference/web.mdx b/shared/video-sdk/develop/video-compositor/reference/web.mdx new file mode 100644 index 000000000..616855616 --- /dev/null +++ b/shared/video-sdk/develop/video-compositor/reference/web.mdx @@ -0,0 +1,61 @@ + + +### API reference + +#### IVideoCompositingExtension + +##### createProcessor + +`createProcessor(): VideoTrackCompositor;` + +Creates a Video Compositor. + +Return value: + +`VideoTrackCompositor`: The `VideoTrackCompositor` object. + +#### IVideoTrack Compositor + +##### createInputEndpoint + +`createInputEndpoint(option: LayerOption): IBaseProcessor;` + +Create the input layer for the video track. + +Parameters: + +- `option`: Layout options for video input. See [LayerOption](#LayerOption) for details. + +Return value: + +`IBaseProcessor`: The `IBaseProcessor` object. + +##### addImage + +`addImage(url: string, option: LayerOption): HTMLImageElement;` + +Create the input layer for the image. If you need to change the picture after calling this method, just modify the property `src` of the `HTMLImageElement` object. + +Parameters: + +- `url`: The following values ​​can be passed in: + - The relative path of the local image. + - The URL of the online image. You need to ensure that the URL can be loaded by the `HTMLImageElement` object and can be accessed across domains. +- `option`: Layout options for video input. See [LayerOption](#LayerOption) for details. + +Return value: + +`HTMLImageElement`: The `HTMLImageElement` object. + +##### removeImage + +`removeImage(imgElement: HTMLImageElement): void;` + +Delete the input layer of the image. + +Parameters: + +- `imgElement`: The `HTMLImageElement` object. + + + diff --git a/shared/video-sdk/reference/_release-notes.mdx b/shared/video-sdk/reference/_release-notes.mdx index 501aade2a..096f79d80 100644 --- a/shared/video-sdk/reference/_release-notes.mdx +++ b/shared/video-sdk/reference/_release-notes.mdx @@ -22,9 +22,10 @@ This page provides the release notes for . - [](#ai-noise-suppression) - [](#virtual-background) + ## Video SDK - + @@ -35,6 +36,7 @@ This page provides the release notes for . + ## Notifications @@ -49,5 +51,4 @@ This page provides the release notes for . ## Virtual Background - diff --git a/shared/video-sdk/reference/release-notes/android.mdx b/shared/video-sdk/reference/release-notes/android.mdx index 03890c395..d419bd7d6 100644 --- a/shared/video-sdk/reference/release-notes/android.mdx +++ b/shared/video-sdk/reference/release-notes/android.mdx @@ -1,11 +1,10 @@ -import KnownIssues from '@docs/shared/video-sdk/reference/known-issues/android.mdx'; If your target platform is Android 12 or higher, add the `android.permission.BLUETOOTH_CONNECT` permission to the `AndroidManifest.xml` file of the Android project to enable the Bluetooth function of the Android system. ### Known issues - +See [Known issues](known-issues). ### v4.3.0 @@ -580,7 +579,7 @@ As of this release, the SDK optimizes the video encoder algorithm and upgrades t Call the `setVideoEncoderConfiguration` method to set the expected video encoding resolution in the video encoding parameters configuration. -
    The increase in the default resolution affects the aggregate resolution and thus the billing rate. See Pricing.
    +
    The increase in the default resolution affects the aggregate resolution and thus the billing rate. See Pricing.
    #### New features @@ -877,10 +876,10 @@ This release optimizes the trigger logic of `onVideoSizeChanged`, which can also This release fixed the following issues. -1. When calling `setVideoEncoderConfigurationEx` in the channel to increase the resolution of the video, it occasionally failed. +1. When calling `setVideoEncoderConfigurationEx` in the channel to increase the resolution of the video, it occasionally failed. 2. In online meeting scenarios, the local user and the remote user might not hear each other after the local user is interrupted by a call. 3. After calling `setCloudProxy` to set the cloud proxy, calling `joinChannelEx` to join multiple channels failed. -4. When using the Agora media player to play videos, after you play and pause the video, and then call the seek method to specify a new position for playback, the video image might remain unchanged; if you call the resume method to resume playback, the video might be played in a speed faster than the original one. +4. When using the Agora media player to play videos, after you play and pause the video, and then call the seek method to specify a new position for playback, the video image might remain unchanged; if you call the resume method to resume playback, the video might be played in a speed faster than the original one. #### API changes @@ -911,7 +910,7 @@ This release fixed the following issues. - `enableDualStreamModeEx` - + **Deprecated** @@ -919,7 +918,7 @@ This release fixed the following issues. **Deleted** -- `enableDualStreamMode` [2/3] +- `enableDualStreamMode` [2/3] ### v4.0.0 @@ -970,7 +969,7 @@ The UHD resolution (4K, 60 fps) is currently in beta and requires certain device High resolution typically means higher performance consumption. To avoid a decrease in experience due to insufficient device performance, Agora recommends that you enable FHD and UHD video resolutions on devices with better performance. -The increase in the default resolution affects the aggregate resolution and thus the billing rate. See Pricing. +The increase in the default resolution affects the aggregate resolution and thus the billing rate. See Pricing. **3. Agora media player** diff --git a/shared/video-sdk/reference/release-notes/flutter.mdx b/shared/video-sdk/reference/release-notes/flutter.mdx index 57fa4e7b4..4a5479e5f 100644 --- a/shared/video-sdk/reference/release-notes/flutter.mdx +++ b/shared/video-sdk/reference/release-notes/flutter.mdx @@ -1,28 +1,8 @@ -import KnownIssues from '@docs/shared/video-sdk/reference/known-issues/flutter.mdx'; ### Known issues - - -### v6.2.6 - -v6.2.6 was released on November 24, 2023. - -#### Issues fixed - -This release fixed the following issues: - -- Issues occurring when using Android 14: - - - When switching between portrait and landscape modes during screen sharing, the screen sharing process was interrupted. To restart screen sharing, users needed to confirm recording the screen in the pop-up window. - - When integrating the SDK, setting the Android `targetSdkVersion` to 34 could cause screen sharing to be unavailable or even cause the app to crash. - - Calling `startScreenCapture` without sharing video (setting `captureVideo` to `false`) and then calling `updateScreenCaptureParameters` to share video (setting `captureVideo` to `true`) resulted in a frozen shared screen on the receiving end. - - When screen sharing in landscape mode, the shared screen seen by the audience was divided into two parts: One side of the screen was compressed and the other side was black. - -- When using an iOS 16 or later device with Bluetooth headphones connected before joining the channel, the audio routing after joining the channel was not as expected: Audio was played from the speaker, not the Bluetooth headphones (iOS). -- In live streaming scenarios, the video on the audience end was occasionally distorted (Android). -- In specific scenarios, such as when the network packet loss rate was high or when the broadcaster left the channel without destroying the engine and then re-joined the channel, the video on the receiving end stuttered or froze. +See [Known issues](known-issues). ### v6.2.6 diff --git a/shared/video-sdk/reference/release-notes/ios.mdx b/shared/video-sdk/reference/release-notes/ios.mdx index 228b8607e..281d28b38 100644 --- a/shared/video-sdk/reference/release-notes/ios.mdx +++ b/shared/video-sdk/reference/release-notes/ios.mdx @@ -1,9 +1,8 @@ -import KnownIssues from '@docs/shared/video-sdk/reference/known-issues/ios.mdx'; ### Known issues - +See [Known issues](known-issues). ### v4.3.0 @@ -325,7 +324,7 @@ v4.2.2 was released on July 27, 2023. 1. **Wildcard token** - This release introduces wildcard tokens. Agora supports setting the channel name used for generating a token as a wildcard character. The token generated can be used to join any channel if you use the same user id. In scenarios involving multiple channels, such as switching between different channels, using a wildcard token can avoid repeated application of tokens every time users joining a new channel, which reduces the pressure on your token server. See [Secure authentication with tokens](/en/video-calling/get-started/authentication-workflow#wildcard-tokens). + This release introduces wildcard tokens. Agora supports setting the channel name used for generating a token as a wildcard character. The token generated can be used to join any channel if you use the same user id. In scenarios involving multiple channels, such as switching between different channels, using a wildcard token can avoid repeated application of tokens every time users joining a new channel, which reduces the pressure on your token server. See [Wildcard Tokens](https://docportal.shengwang.cn/cn/live-streaming-premium-4.x/wildcard_token?platform=All%20Platforms).
    All 4.x SDKs support using wildcard tokens.
    @@ -333,7 +332,7 @@ v4.2.2 was released on July 27, 2023. This release adds `preloadChannelByToken [1/2]` and `preloadChannelByToken [2/2]` methods, which allows a user whose role is set as audience to preload channels before joining one. Calling the method can help shortening the time of joining a channel, thus reducing the time it takes for audience members to hear and see the host. - When preloading more than one channels, Agora recommends that you use a wildcard token for preloading to avoid repeated application of tokens every time you joining a new channel, thus saving the time for switching between channels. See [Secure authentication with tokens](/en/video-calling/get-started/authentication-workflow#wildcard-tokens). + When preloading more than one channels, Agora recommends that you use a wildcard token for preloading to avoid repeated application of tokens every time you joining a new channel, thus saving the time for switching between channels. See [Wildcard Token](https://docportal.shengwang.cn/cn/live-streaming-premium-4.x/wildcard_token?platform=All%20Platforms). 3. **Customized background color of video canvas** diff --git a/shared/video-sdk/reference/release-notes/react-js.mdx b/shared/video-sdk/reference/release-notes/react-js.mdx index 20a46a5c7..97ab97727 100644 --- a/shared/video-sdk/reference/release-notes/react-js.mdx +++ b/shared/video-sdk/reference/release-notes/react-js.mdx @@ -1,71 +1,8 @@ -### v2.1.0 +### v2.0.0-alpha.0 -v2.1.0 was released on January 5, 2024. +This is the first alpha release of Video SDK for ReactJS. -This version updates the built-in SDK for Web in the SDK for React JS to version 4.20.0. Check the related changes in the [Web SDK release notes](../overview/release-notes?platform=web#v4200). -### v2.0.0 - -v2.0.0 was released on December 22, 2023. - -#### Compatibility changes - -**SDK structural optimization** - -The SDK for ReactJS is developed based on the SDK for Web v4.x. To further enhance usability, this version incorporates all Web SDK APIs into the ReactJS SDK, eliminating the need to integrate the Web SDK separately. - -Upon upgrading to this version, make the following modifications: - -- Reintegrate the React SDK. Taking npm as an example: - - ```sh - # Remove existing dependencies - npm uninstall agora-rtc-react agora-rtc-sdk-ng - # Reinstall dependencies - npm install agora-rtc-react - ``` - -- Adjust the import of the `AgoraRTC` interface from the Web SDK. Taking the combined import of `AgoraRTC` and `AgoraRTCProvider` as an example: - - ```jsx - // Before this version - import AgoraRTC from "agora-rtc-sdk-ng"; - import { AgoraRTCProvider } from "agora-rtc-react"; - // As of this version - import AgoraRTC, { AgoraRTCProvider } from "agora-rtc-react" - ``` - -**NetworkQuality renaming** - -To avoid redundancy with the Web SDK's API, this version makes the following changes to the ReactJS SDK's `NetworkQuality` interface: - -- Rename `NetworkQuality` to `NetworkQualityEx` and have `NetworkQualityEx` inherit the `NetworkQuality` interface from the Web SDK. -- Rename the `uplink` and `downlink` properties to `uplinkNetworkQuality` and `downlinkNetworkQuality`, respectively. - -If you use the `NetworkQuality` interface from the ReactJS SDK, make the necessary code modifications after upgrading to this version. - -#### Improvements - -This version adds the `cameraVideoTrackConfig` parameter to `useLocalCameraTrack`, enabling you to set video capture configurations such as capture devices and encoder when creating a camera video track. - -#### Fixed issues - -This version fixed the issue that the SDK threw the error `CAN_NOT_PUBLISH_MULTIPLE_VIDEO_TRACKS` in Firefox's developer mode. - -#### API Changes - -**Added** - -- `cameraVideoTrackConfig` to `useLocalCameraTrack` - -**Modified** - -- `NetworkQuality` to `NetworkQualityEx` - - `uplink` to `uplinkNetworkQuality` - - `downlink` to `downlinkNetworkQuality` - - - - +
    \ No newline at end of file diff --git a/shared/video-sdk/reference/release-notes/react-native.mdx b/shared/video-sdk/reference/release-notes/react-native.mdx index 8fde9eb1c..c9e5343b1 100644 --- a/shared/video-sdk/reference/release-notes/react-native.mdx +++ b/shared/video-sdk/reference/release-notes/react-native.mdx @@ -1,28 +1,8 @@ -import KnownIssues from '@docs/shared/video-sdk/reference/known-issues/react-native.mdx'; ### Known issues - - -### v4.2.6 - -v4.2.6 was released on November 24, 2023. - -#### Issues fixed - -This release fixed the following issues: - -- Issues occurring when using Android 14: - - - When switching between portrait and landscape modes during screen sharing, the screen sharing process was interrupted. To restart screen sharing, users needed to confirm recording the screen in the pop-up window. - - When integrating the SDK, setting the Android `targetSdkVersion` to 34 could cause screen sharing to be unavailable or even cause the app to crash. - - Calling `startScreenCapture` without sharing video (setting `captureVideo` to `false`) and then calling `updateScreenCaptureParameters` to share video (setting `captureVideo` to `true`) resulted in a frozen shared screen on the receiving end. - - When screen sharing in the landscape mode, the shared screen seen by the audience was divided into two parts: One side of the screen was compressed, the other side was black. - -- When using an iOS 16 or later device with Bluetooth headphones connected before joining the channel, the audio routing after joining the channel was not as expected: Audio was played from the speaker, not the Bluetooth headphones (iOS). -- In live streaming scenarios, the video on the audience end was occasionally distorted (Android). -- In specific scenarios, such as when the network packet loss rate was high or when the broadcaster left the channel without destroying the engine and then re-joined the channel, the video on the receiving end stuttered or froze. +See [Known issues](known-issues). ### v4.2.6 diff --git a/shared/video-sdk/reference/release-notes/unity.mdx b/shared/video-sdk/reference/release-notes/unity.mdx index c40e9ebd2..84cdbfa9e 100644 --- a/shared/video-sdk/reference/release-notes/unity.mdx +++ b/shared/video-sdk/reference/release-notes/unity.mdx @@ -1,28 +1,8 @@ -import KnownIssues from '@docs/shared/video-sdk/reference/known-issues/unity.mdx'; ### Known issues - - -### v4.2.6 - -v4.2.6 was released on November 24, 2023. - -#### Issues fixed - -This release fixed the following issues: - -- Issues occurring when using Android 14: - - - When switching between portrait and landscape modes during screen sharing, the screen sharing process was interrupted. To restart screen sharing, users needed to confirm recording the screen in the pop-up window. - - When integrating the SDK, setting the Android `targetSdkVersion` to 34 could cause screen sharing to be unavailable or even cause the app to crash. - - Calling `StartScreenCapture`[1/2] without sharing video (setting `captureVideo` to `false`) and then calling `UpdateScreenCaptureParameters` to share video (setting `captureVideo`to `true`) resulted in a frozen shared screen on the receiving end. - - When screen sharing in the landscape mode, the shared screen seen by the audience was divided into two parts: One side of the screen was compressed, the other side was black. - -- When using an iOS 16 or later device with Bluetooth headphones connected before joining the channel, the audio routing after joining the channel was not as expected: Audio was played from the speaker, not the Bluetooth headphones (iOS). -- In live streaming scenarios, the video on the audience end was occasionally distorted (Android). -- In specific scenarios, such as when the network packet loss rate was high or when the broadcaster left the channel without destroying the engine and then re-joined the channel, the video on the receiving end stuttered or froze. +See [Known issues](known-issues). ### v4.2.6 diff --git a/shared/video-sdk/reference/release-notes/web.mdx b/shared/video-sdk/reference/release-notes/web.mdx index 4f576d5b2..f4053d460 100644 --- a/shared/video-sdk/reference/release-notes/web.mdx +++ b/shared/video-sdk/reference/release-notes/web.mdx @@ -1,15 +1,15 @@ + -import KnownIssues from '@docs/shared/video-sdk/reference/known-issues/web.mdx'; 4.x is a JavaScript and TypeScript library loaded by an HTML web page. The SDK library uses APIs in the web browser to establish connections and control the communication, and services. ### Compatibility -See [Supported platforms](../overview/supported-platforms). +See [Supported platforms](../reference/supported-platforms). ### Known issues - +See [Known issues](known-issues). ### v4.20.1 @@ -144,8 +144,6 @@ This release fixes the issue that an error occurred when using Webpack 4.x to bu v4.19.0 was released on September 25, 2023. -After integrating this version of the SDK into your app, you might encounter an error if you use Webpack 4.x to bundle your app. To resolve this issue:
    • If you intend to use Webpack 4.x, upgrade the SDK to v4.19.1.
    • If you prefer to continue using v4.19.0, Agora recommends bundling your app with Webpack 5.x or other tools such as Vite.
    - #### Compatibility changes **Dynamic low-quality video profile adjustment** @@ -156,6 +154,10 @@ As of this release, you can call `setLowStreamParameter` after `publish` to dyna #### New features +**End-to-end media encryption (Beta)** + +For scenarios demanding high data security, this release introduces end-to-end media encryption in addition to [Secure channels with encryption](../develop/media-stream-encryption?platform=web). This feature encrypts media streams on the sender's device and decrypts them on the receiver's device, ensuring that data remains inaccessible to third parties during transmission. To enable end-to-end media encryption, please contact technical support. + **Tree shaking support** As of this release, the SDK supports [tree shaking](https://webpack.js.org/guides/tree-shaking/), which removes unused code during the build process, reducing your app's size after integrating the SDK. To use this feature, see [App size optimization](../reference/downloads?platform=web). @@ -175,6 +177,10 @@ See `setDevice` for details.
    +** extension (Beta)** + +As of this release, the SDK can be used together with the extension to intelligently enhance video quality without changing the resolution, thereby improving the viewing experience. See [Super Clarity](../../extensions-marketplace/develop/integrate/superclarity). + **Third-party video moderation** As of this release, the SDK supports integration with third-party video moderation services. @@ -187,11 +193,6 @@ See `setImageModeration` in the API Reference for details. -**Beta features** - -End-to-end media encryption and Super Clarity extension are released in beta. -See [beta documentation](https://docs-beta.agora.io/en/video-calling/reference/release-notes) for details. - #### Improvements **Optimized automatic mode** @@ -246,16 +247,6 @@ This release fixes the following issues: - `AgoraRTCClient.on("join-fallback-to-proxy")` -### v4.18.3 - -v4.18.3 was released on September 20, 2023. - -#### Fixed issues - -v4.18.3 fixes the following issue: - -- The SDK failed to obtain audio and video statistics on Safari 17. - ### v4.18.2 v4.18.2 was released on July 11, 2023. @@ -303,6 +294,20 @@ v4.18.0 was released on June 12, 2023. #### New features +**Scalable Video Coding (Beta)** + +As of v4.18.0, the SDK supports enabling Scalable Video Coding (SVC) when using the VP9 codec. SVC allows adaptive adjustment of video encoding quality based on users' network conditions and device performance, ensuring an optimal viewing experience for all users. + +To enable SVC, contact [support@agora.io](mailto:support@agora.io). + +**Access to encoded data (Beta)** + +As of v4.18.0, you can access the SDK's encoded audio and video data. To enable this feature, contact [support@agora.io](mailto:support@agora.io). + +**Enhanced connection experience in restricted networks (Beta)** + +v4.18.0 introduces new media connection strategies to optimize the connection experience in restricted network environments. To enable this feature, contact [support@agora.io](mailto:support@agora.io). + **Variable playback speed for audio files** To facilitate changing the playback speed of audio effect or music files, v4.18.0 modifies the `IBufferSourceAudioTrack` class as follows: @@ -340,9 +345,6 @@ You can continue using the previous approach. However, for higher flexibility an - `onSecurityPolicyViolation` - `onAudioAutoplayFailed`: Deprecated, see `onAutoplayFailed` instead -Scalable Video Coding, access to encoded data, and enhanced connection experience in restricted networks are released in beta. -See [beta documentation](https://docs-beta.agora.io/en/video-calling/reference/release-notes) for details. - #### Improvements **Shorter time to join channel** @@ -455,6 +457,18 @@ v4.17.0 was released on March 22, 2023. #### New features +**1,000 hosts in a channel (Beta functionality)** + +As of v4.17.0, a single channel can support up to 1,000 concurrent online hosts, who can publish media streams at the same time. +The number of audience members in a channel is unlimited. Each host or audience member can subscribe to a maximum of 50 hosts at the same time. + +To turn on this feature, contact [support@agora.io](mailto:support@agora.io). + +**Local video compositing (Beta functionality)** + +As of v4.17.0, the SDK can be used with the video compositing extension to combine multiple video streams and images +into one video stream locally. You can also use the Virtual Background extension to achieve a portrait-in-picture effect. + **Dynamic switching of local video streams** This release adds the `ILocalVideoTrack.replaceTrack` method to increase the flexibility to switch local video tracks. @@ -1055,7 +1069,7 @@ To strengthen the security of the key, v4.5.0 adds two encryption modes, `"aes-1 **Network geofencing** -As of v4.5.0, when calling setArea to specify the region for connection, you can use the `areaCode` parameter to specify a large region and use the `excludedArea` parameter to specify a small region. The region for connection is the large region excluding the small region. You can only specify the large region as `"GLOBAL"`. For details, see [Network Geofencing](../../video-calling/enable-features/geofencing). +As of v4.5.0, when calling setArea to specify the region for connection, you can use the `areaCode` parameter to specify a large region and use the `excludedArea` parameter to specify a small region. The region for connection is the large region excluding the small region. You can only specify the large region as `"GLOBAL"`. For details, see [Network Geofencing](../../video-calling/develop/geofencing). **The preset video encoder configurations for screen sharing** @@ -1443,4 +1457,4 @@ v4.0.0 fixed the following issues: - The `LocalTrack.getUserId` method -
    \ No newline at end of file +
    diff --git a/shared/voice-sdk/reference/release-notes/android.mdx b/shared/voice-sdk/reference/release-notes/android.mdx index 69a0a591c..a401d50f8 100644 --- a/shared/voice-sdk/reference/release-notes/android.mdx +++ b/shared/voice-sdk/reference/release-notes/android.mdx @@ -703,4 +703,5 @@ This release adds `voicePitch` in `AudioVolumeInfo` of `onAudioVolumeIndication` This release adds the `onPermissionError` method, which is automatically reported when the audio capture device or camera does not obtain the appropriate permission. You can enable the corresponding device permission according to the prompt of the callback. -
    \ No newline at end of file + + diff --git a/shared/voice-sdk/reference/release-notes/flutter.mdx b/shared/voice-sdk/reference/release-notes/flutter.mdx index 8c24ce66f..2c25987c6 100644 --- a/shared/voice-sdk/reference/release-notes/flutter.mdx +++ b/shared/voice-sdk/reference/release-notes/flutter.mdx @@ -1,19 +1,4 @@ -import KnownIssues from '@docs/shared/voice-sdk/reference/known-issues/flutter.mdx'; - -### Known issues - - - -### v6.2.6 - -v6.2.6 was released on November 24, 2023. - -#### Issues fixed - -This release fixed the following issue: - -- When using an iOS 16 or later device with Bluetooth headphones connected before joining the channel, the audio routing after joining the channel was not as expected: Audio was played from the speaker, not the Bluetooth headphones (iOS). ### Known issues diff --git a/shared/voice-sdk/reference/release-notes/index.mdx b/shared/voice-sdk/reference/release-notes/index.mdx index 303f474b8..9530ce6f5 100644 --- a/shared/voice-sdk/reference/release-notes/index.mdx +++ b/shared/voice-sdk/reference/release-notes/index.mdx @@ -2,7 +2,6 @@ import Android from './android.mdx'; import Unity from './unity.mdx'; import Flutter from './flutter.mdx'; import ReactNative from './react-native.mdx'; -import ReactJS from './react-js.mdx'; import Electron from './electron.mdx'; import Ios from './ios.mdx'; import Macos from './macos.mdx'; @@ -25,7 +24,6 @@ This page provides the release notes for the -import KnownIssues from '@docs/shared/voice-sdk/reference/known-issues/ios.mdx'; ### Known issues - +See [Known issues](known-issues). ### v4.3.0 diff --git a/shared/voice-sdk/reference/release-notes/react-js.mdx b/shared/voice-sdk/reference/release-notes/react-js.mdx deleted file mode 100644 index 5105761d2..000000000 --- a/shared/voice-sdk/reference/release-notes/react-js.mdx +++ /dev/null @@ -1,65 +0,0 @@ - - -### v2.0.0 - -v2.0.0 was released on December XX, 2023. - -#### Compatibility changes - -**SDK structural optimization** - -The SDK for ReactJS is developed based on the SDK for Web v4.x. To further enhance usability, this version incorporates all Web SDK APIs into the ReactJS SDK, eliminating the need to integrate the Web SDK separately. - -Upon upgrading to this version, make the following modifications: - -- Reintegrate the React SDK. Taking npm as an example: - - ```sh - # Remove existing dependencies - npm uninstall agora-rtc-react agora-rtc-sdk-ng - # Reinstall dependencies - npm install agora-rtc-react - ``` - -- Adjust the import of the `AgoraRTC` interface from the Web SDK. Taking the combined import of `AgoraRTC` and `AgoraRTCProvider` as an example: - - ```jsx - // Before this version - import AgoraRTC from "agora-rtc-sdk-ng"; - import { AgoraRTCProvider } from "agora-rtc-react"; - // As of this version - import AgoraRTC, { AgoraRTCProvider } from "agora-rtc-react" - ``` - -**NetworkQuality renaming** - -To avoid redundancy with the Web SDK's API, this version makes the following changes to the ReactJS SDK's `NetworkQuality` interface: - -- Rename `NetworkQuality` to `NetworkQualityEx` and have `NetworkQualityEx` inherit the `NetworkQuality` interface from the Web SDK. -- Rename the `uplink` and `downlink` properties to `uplinkNetworkQuality` and `downlinkNetworkQuality`, respectively. - -If you use the `NetworkQuality` interface from the ReactJS SDK, make the necessary code modifications after upgrading to this version. - -#### Improvements - -This version adds the `cameraVideoTrackConfig` parameter to `useLocalCameraTrack`, enabling you to set video capture configurations such as capture devices and encoder when creating a camera video track. - -#### Fixed issues - -This version fixed the issue that the SDK threw the error `CAN_NOT_PUBLISH_MULTIPLE_VIDEO_TRACKS` in Firefox's developer mode. - -#### API Changes - -**Added** - -- `cameraVideoTrackConfig` to `useLocalCameraTrack` - -**Modified** - -- `NetworkQuality` to `NetworkQualityEx` - - `uplink` to `uplinkNetworkQuality` - - `downlink` to `downlinkNetworkQuality` - - - - \ No newline at end of file diff --git a/shared/voice-sdk/reference/release-notes/react-native.mdx b/shared/voice-sdk/reference/release-notes/react-native.mdx index 163ccf6ce..7933dc321 100644 --- a/shared/voice-sdk/reference/release-notes/react-native.mdx +++ b/shared/voice-sdk/reference/release-notes/react-native.mdx @@ -1,19 +1,4 @@ -import KnownIssues from '@docs/shared/voice-sdk/reference/known-issues/react-native.mdx'; - -### Known issues - - - -### v4.2.6 - -v4.2.6 was released on November 24, 2023. - -#### Issues fixed - -This release fixed the following issue: - -- When using an iOS 16 or later device with Bluetooth headphones connected before joining the channel, the audio routing after joining the channel was not as expected: Audio was played from the speaker, not the Bluetooth headphones (iOS). ### Known issues diff --git a/shared/voice-sdk/reference/release-notes/unity.mdx b/shared/voice-sdk/reference/release-notes/unity.mdx index dd3e342ad..ead7a1fff 100644 --- a/shared/voice-sdk/reference/release-notes/unity.mdx +++ b/shared/voice-sdk/reference/release-notes/unity.mdx @@ -1,19 +1,4 @@ -import KnownIssues from '@docs/shared/voice-sdk/reference/known-issues/unity.mdx'; - -### Known issues - - - -### v4.2.6 - -v4.2.6 was released on November 24, 2023. - -#### Issues fixed - -This release fixed the following issue: - -- When using an iOS 16 or later device with Bluetooth headphones connected before joining the channel, the audio routing after joining the channel was not as expected: Audio was played from the speaker, not the Bluetooth headphones (iOS). ### Known issues diff --git a/video-calling/enable-features/image-enhancement.mdx b/video-calling/enable-features/image-enhancement.mdx new file mode 100644 index 000000000..7368d7d75 --- /dev/null +++ b/video-calling/enable-features/image-enhancement.mdx @@ -0,0 +1,13 @@ +--- +title: 'Image enhancer (beta)' +sidebar_position: 17 +type: docs +description: > + Gives you granular control over the degree of image enhancement, such as skin lightening, skin smoothing, and red saturation. +--- + +import ImageEnhancer from '@docs/shared/extensions-marketplace/image-enhancement.mdx'; + +export const toc = [{}]; + + diff --git a/video-calling/enable-features/video-compositor.mdx b/video-calling/enable-features/video-compositor.mdx new file mode 100644 index 000000000..be93db461 --- /dev/null +++ b/video-calling/enable-features/video-compositor.mdx @@ -0,0 +1,13 @@ +--- +title: 'Video Compositor (beta)' +sidebar_position: 17 +type: docs +description: > + Gives you granular control over the degree of image enhancement, such as skin lightening, skin smoothing, and red saturation. +--- + +import VideoCompositor from '@docs/shared/video-sdk/develop/_video-compositor.mdx'; + +export const toc = [{}]; + + \ No newline at end of file From 10d3d847cef6a4feeb9bc2327ab810fd62c17e21 Mon Sep 17 00:00:00 2001 From: Kishan Dhakan Date: Wed, 28 Feb 2024 14:29:49 +0530 Subject: [PATCH 3/7] add media gateway doc set, rtt doc set, super clarity doc, video compositor doc, image enhancer doc, keep references as in beta --- shared/video-sdk/reference/release-notes/android.mdx | 2 +- shared/video-sdk/reference/release-notes/ios.mdx | 2 +- shared/voice-sdk/reference/release-notes/ios.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shared/video-sdk/reference/release-notes/android.mdx b/shared/video-sdk/reference/release-notes/android.mdx index d419bd7d6..c56a50123 100644 --- a/shared/video-sdk/reference/release-notes/android.mdx +++ b/shared/video-sdk/reference/release-notes/android.mdx @@ -1070,4 +1070,4 @@ This release improves the implementation logic of `startPreview`. You can call t **6. Video types of subscription** You can call the `setRemoteDefaultVideoStreamType` method to choose the video stream type when subscribing to streams. - \ No newline at end of file + diff --git a/shared/video-sdk/reference/release-notes/ios.mdx b/shared/video-sdk/reference/release-notes/ios.mdx index 281d28b38..315ea9aa6 100644 --- a/shared/video-sdk/reference/release-notes/ios.mdx +++ b/shared/video-sdk/reference/release-notes/ios.mdx @@ -1462,4 +1462,4 @@ This release can achieve the same switching speed as `switchChannelByToken` in v **2. Voice pitch of the local user** This release adds `voicePitch` in `AgoraRtcAudioVolumeInfo` of `reportAudioVolumeIndicationOfSpeakers`. You can use `voicePitch` to get the local user's voice pitch and perform business functions such as rating for singing. - \ No newline at end of file + diff --git a/shared/voice-sdk/reference/release-notes/ios.mdx b/shared/voice-sdk/reference/release-notes/ios.mdx index f0bfc7892..6c7c4334d 100644 --- a/shared/voice-sdk/reference/release-notes/ios.mdx +++ b/shared/voice-sdk/reference/release-notes/ios.mdx @@ -678,4 +678,4 @@ This release can achieve the same switching speed as `switchChannelByToken` in v This release adds `voicePitch` in `AgoraRtcAudioVolumeInfo` of `reportAudioVolumeIndicationOfSpeakers`. You can use `voicePitch` to get the local user's voice pitch and perform business functions such as rating for singing. - \ No newline at end of file + From b5ca781c383b4e6e0acd3174322341333d148f22 Mon Sep 17 00:00:00 2001 From: atovpeko Date: Tue, 5 Mar 2024 11:58:05 +0200 Subject: [PATCH 4/7] Media Gateway and RTT added --- media-gateway/develop/_category_.json | 6 ++ media-gateway/get-started/_category_.json | 6 ++ media-gateway/get-started/quickstart.mdx | 13 +++++ media-gateway/overview/_category_.json | 6 ++ media-gateway/overview/core-concepts.mdx | 14 +++++ media-gateway/overview/product-overview.mdx | 54 ++++++++++++++++++ media-gateway/reference/_category_.json | 6 ++ media-gateway/reference/best-practice.mdx | 13 +++++ media-gateway/reference/glossary.mdx | 14 +++++ .../reference/manage-agora-account.mdx | 14 +++++ media-gateway/reference/release-notes.mdx | 14 +++++ media-gateway/reference/security.mdx | 14 +++++ .../get-started/_category_.json | 6 ++ .../get-started/get-started.mdx | 14 +++++ .../overview/_category_.json | 6 ++ .../overview/core-concepts.mdx | 15 +++++ .../overview/product-overview.mdx | 55 +++++++++++++++++++ .../reference/_category_.json | 6 ++ .../reference/glossary.mdx | 14 +++++ .../reference/manage-agora-account.mdx | 14 +++++ real-time-transcription/reference/pricing.mdx | 41 ++++++++++++++ .../reference/security.mdx | 14 +++++ 22 files changed, 359 insertions(+) create mode 100644 media-gateway/develop/_category_.json create mode 100644 media-gateway/get-started/_category_.json create mode 100644 media-gateway/get-started/quickstart.mdx create mode 100644 media-gateway/overview/_category_.json create mode 100644 media-gateway/overview/core-concepts.mdx create mode 100644 media-gateway/overview/product-overview.mdx create mode 100644 media-gateway/reference/_category_.json create mode 100644 media-gateway/reference/best-practice.mdx create mode 100644 media-gateway/reference/glossary.mdx create mode 100644 media-gateway/reference/manage-agora-account.mdx create mode 100644 media-gateway/reference/release-notes.mdx create mode 100644 media-gateway/reference/security.mdx create mode 100644 real-time-transcription/get-started/_category_.json create mode 100644 real-time-transcription/get-started/get-started.mdx create mode 100644 real-time-transcription/overview/_category_.json create mode 100644 real-time-transcription/overview/core-concepts.mdx create mode 100644 real-time-transcription/overview/product-overview.mdx create mode 100644 real-time-transcription/reference/_category_.json create mode 100644 real-time-transcription/reference/glossary.mdx create mode 100644 real-time-transcription/reference/manage-agora-account.mdx create mode 100644 real-time-transcription/reference/pricing.mdx create mode 100644 real-time-transcription/reference/security.mdx diff --git a/media-gateway/develop/_category_.json b/media-gateway/develop/_category_.json new file mode 100644 index 000000000..bee160082 --- /dev/null +++ b/media-gateway/develop/_category_.json @@ -0,0 +1,6 @@ +{ + "position": 3, + "label": "Develop", + "collapsible": true, + "link": null +} diff --git a/media-gateway/get-started/_category_.json b/media-gateway/get-started/_category_.json new file mode 100644 index 000000000..8d46f797b --- /dev/null +++ b/media-gateway/get-started/_category_.json @@ -0,0 +1,6 @@ +{ + "position": 2, + "label": "Get started", + "collapsible": true, + "link": null +} diff --git a/media-gateway/get-started/quickstart.mdx b/media-gateway/get-started/quickstart.mdx new file mode 100644 index 000000000..420485cc3 --- /dev/null +++ b/media-gateway/get-started/quickstart.mdx @@ -0,0 +1,13 @@ +--- +title: 'Media Gateway quickstart' +sidebar_position: 1 +type: docs +description: > + Stream directly with RTMP/SRT protocol to Agora RTC channels. +--- + +import MEDIAGateway from '@docs/shared/media-gateway/get-started/_quickstart.mdx'; + +export const toc = [{}]; + + diff --git a/media-gateway/overview/_category_.json b/media-gateway/overview/_category_.json new file mode 100644 index 000000000..9874239b6 --- /dev/null +++ b/media-gateway/overview/_category_.json @@ -0,0 +1,6 @@ +{ + "position": 1, + "label": "Overview", + "collapsible": true, + "link": null +} diff --git a/media-gateway/overview/core-concepts.mdx b/media-gateway/overview/core-concepts.mdx new file mode 100644 index 000000000..d12bcbf7a --- /dev/null +++ b/media-gateway/overview/core-concepts.mdx @@ -0,0 +1,14 @@ +--- +title: 'Core concepts' +sidebar_position: 2 +type: docs +platform_selector: false +description: > + Ideas that are central to developing with Agora. +--- + +import CoreConcepts from '@docs/shared/common/_core-concepts.mdx'; + +export const toc = [{}]; + + diff --git a/media-gateway/overview/product-overview.mdx b/media-gateway/overview/product-overview.mdx new file mode 100644 index 000000000..4fa432d77 --- /dev/null +++ b/media-gateway/overview/product-overview.mdx @@ -0,0 +1,54 @@ +--- +title: 'Product overview' +sidebar_position: 1 +platform_selector: false +description: > + Stream directly with RTMP/SRT protocol to Agora RTC channels.. +--- + + + + allows users to directly push media streams into Agora’s Real-Time Voice and Video channels using the +RTMP/SRT protocol. To facilitate distribution, Media Gateway also allows users to perform advanced transcoding +processing on media streams. + + \ No newline at end of file diff --git a/media-gateway/reference/_category_.json b/media-gateway/reference/_category_.json new file mode 100644 index 000000000..d540fd6db --- /dev/null +++ b/media-gateway/reference/_category_.json @@ -0,0 +1,6 @@ +{ + "position": 4, + "label": "Reference", + "collapsible": true, + "link": null +} diff --git a/media-gateway/reference/best-practice.mdx b/media-gateway/reference/best-practice.mdx new file mode 100644 index 000000000..ab801a37d --- /dev/null +++ b/media-gateway/reference/best-practice.mdx @@ -0,0 +1,13 @@ +--- +title: 'Integration best practice' +sidebar_position: 1 +type: docs +description: > + Best practice for using Media Gateway and its RESTful API. +--- + +import BestPractice from '@docs/shared/media-gateway/reference/_best-practice.mdx'; + +export const toc = [{}]; + + diff --git a/media-gateway/reference/glossary.mdx b/media-gateway/reference/glossary.mdx new file mode 100644 index 000000000..97b410dce --- /dev/null +++ b/media-gateway/reference/glossary.mdx @@ -0,0 +1,14 @@ +--- +title: 'Glossary' +sidebar_position: 5 +type: docs +platform_selector: false +description: > + A list of terms used in Agora documentation. +--- + +import Glossary from '@docs/shared/common/_glossary.mdx'; + +export const toc = [{}]; + + \ No newline at end of file diff --git a/media-gateway/reference/manage-agora-account.mdx b/media-gateway/reference/manage-agora-account.mdx new file mode 100644 index 000000000..fddaaad75 --- /dev/null +++ b/media-gateway/reference/manage-agora-account.mdx @@ -0,0 +1,14 @@ +--- +title: 'Agora account management' +sidebar_position: 2 +type: docs +platform_selector: false +description: > + Create, manage and update your Agora account. +--- + +import ManageAccount from '@docs/shared/common/_manage-agora-account.mdx'; + +export const toc = [{}]; + + \ No newline at end of file diff --git a/media-gateway/reference/release-notes.mdx b/media-gateway/reference/release-notes.mdx new file mode 100644 index 000000000..d42c50347 --- /dev/null +++ b/media-gateway/reference/release-notes.mdx @@ -0,0 +1,14 @@ +--- +title: 'Release notes' +sidebar_position: 4 +type: docs +description: > + Shows Media Gateway's past releases. +template: 'platform' +--- + +import ReleaseNotes from '@docs/shared/media-gateway/reference/_release-notes.mdx'; + +export const toc = [{}]; + + diff --git a/media-gateway/reference/security.mdx b/media-gateway/reference/security.mdx new file mode 100644 index 000000000..1397dd902 --- /dev/null +++ b/media-gateway/reference/security.mdx @@ -0,0 +1,14 @@ +--- +title: 'Security' +sidebar_position: 3 +type: docs +platform_selector: false +description: > + How Agora handles security. +--- + +import Security from '@docs/shared/common/_security.mdx'; + +export const toc = [{}]; + + \ No newline at end of file diff --git a/real-time-transcription/get-started/_category_.json b/real-time-transcription/get-started/_category_.json new file mode 100644 index 000000000..8d46f797b --- /dev/null +++ b/real-time-transcription/get-started/_category_.json @@ -0,0 +1,6 @@ +{ + "position": 2, + "label": "Get started", + "collapsible": true, + "link": null +} diff --git a/real-time-transcription/get-started/get-started.mdx b/real-time-transcription/get-started/get-started.mdx new file mode 100644 index 000000000..7ae2ea907 --- /dev/null +++ b/real-time-transcription/get-started/get-started.mdx @@ -0,0 +1,14 @@ +--- +title: 'Quickstart' +sidebar_position: 1 +type: docs +description: > + Transcribe audio content of a host's media stream into written words in real time. +--- + +import RealTimeTranscription from '@docs/shared/video-sdk/develop/_real-time-transcription.mdx'; + +export const toc = [{}]; + + + diff --git a/real-time-transcription/overview/_category_.json b/real-time-transcription/overview/_category_.json new file mode 100644 index 000000000..9874239b6 --- /dev/null +++ b/real-time-transcription/overview/_category_.json @@ -0,0 +1,6 @@ +{ + "position": 1, + "label": "Overview", + "collapsible": true, + "link": null +} diff --git a/real-time-transcription/overview/core-concepts.mdx b/real-time-transcription/overview/core-concepts.mdx new file mode 100644 index 000000000..f0fc2ba50 --- /dev/null +++ b/real-time-transcription/overview/core-concepts.mdx @@ -0,0 +1,15 @@ +--- +title: 'Core concepts' +sidebar_position: 2 +type: docs +platform_selector: false +description: > + Ideas that are central to developing with Agora. +--- + +import CoreConcepts from '@docs/shared/common/_core-concepts.mdx'; + +export const toc = [{}]; + + + diff --git a/real-time-transcription/overview/product-overview.mdx b/real-time-transcription/overview/product-overview.mdx new file mode 100644 index 000000000..b834dfd0c --- /dev/null +++ b/real-time-transcription/overview/product-overview.mdx @@ -0,0 +1,55 @@ +--- +title: 'Product overview' +sidebar_position: 1 +platform_selector: false +description: > + Create a better user experience with the most accurate live transcription and subtitling. +--- + + + + +Agora enables you to instantly transcribe speech to text for live audio and video. Channel-based live transcription allows you to distribute live captions to all participants in channel while only paying for the duration of a channel—not the number of users. + + + + \ No newline at end of file diff --git a/real-time-transcription/reference/_category_.json b/real-time-transcription/reference/_category_.json new file mode 100644 index 000000000..d540fd6db --- /dev/null +++ b/real-time-transcription/reference/_category_.json @@ -0,0 +1,6 @@ +{ + "position": 4, + "label": "Reference", + "collapsible": true, + "link": null +} diff --git a/real-time-transcription/reference/glossary.mdx b/real-time-transcription/reference/glossary.mdx new file mode 100644 index 000000000..6af6ebb7e --- /dev/null +++ b/real-time-transcription/reference/glossary.mdx @@ -0,0 +1,14 @@ +--- +title: 'Glossary' +sidebar_position: 10 +type: docs +platform_selector: false +description: > + A list of terms used in Agora documentation. +--- + +import Glossary from '@docs/shared/common/_glossary.mdx'; + +export const toc = [{}]; + + \ No newline at end of file diff --git a/real-time-transcription/reference/manage-agora-account.mdx b/real-time-transcription/reference/manage-agora-account.mdx new file mode 100644 index 000000000..c5f787a17 --- /dev/null +++ b/real-time-transcription/reference/manage-agora-account.mdx @@ -0,0 +1,14 @@ +--- +title: 'Agora account management' +sidebar_position: 9 +type: docs +platform_selector: false +description: > + Create, manage and update your Agora account. +--- + +import ManageAccount from '@docs/shared/common/_manage-agora-account.mdx'; + +export const toc = [{}]; + + diff --git a/real-time-transcription/reference/pricing.mdx b/real-time-transcription/reference/pricing.mdx new file mode 100644 index 000000000..e5a9df319 --- /dev/null +++ b/real-time-transcription/reference/pricing.mdx @@ -0,0 +1,41 @@ +--- +title: 'Pricing' +sidebar_position: 1 +type: docs +platform_selector: false +description: > + Introduces the billing policy for Real-Time Transcription. +--- + +export const toc = [{}]; + +This page introduces the billing policy for the add-on provided by Agora. + +Your billing details may differ if you have signed a contract with Agora. + +## Overview + +Agora calculates the billing of all projects under your Agora account on a monthly basis. Billing begins once you +enable . + + +## Transcription fee + +The Agora streaming server charges you when transcribe the subscribed streams. Agora's free-of-charge policy for the +first 10,000 minutes does not apply to the transcription fee. + + +## Unit price for transcription + +[Real-Time Transcription](../enable-features/real-time-transcription) takes the audio content of a host's media stream +and transcribes it into written words in real time. Agora charges for the time that is enabled in a channel, +which includes transcription for the active host. Transcription is available for up to 3 hosts speaking at the same time. +If several hosts speak simultaneously, Agora also charges for the time of speaking of each additional host using +the same pricing. Also note that when you enable , creates a cloud audience member who subscribes to the audio in the channel. +The usage for this audience member is added to your bill. + +|Usage, minutes per month |Pricing, US$/1,000 minutes| +|--------------------|--------------------------| +|Above 0 | 16.99 | + +Contact sales@agora.io to get a discount. diff --git a/real-time-transcription/reference/security.mdx b/real-time-transcription/reference/security.mdx new file mode 100644 index 000000000..878151a0d --- /dev/null +++ b/real-time-transcription/reference/security.mdx @@ -0,0 +1,14 @@ +--- +title: 'Security' +sidebar_position: 11 +type: docs +platform_selector: false +description: > + How Agora handles security. +--- + +import Security from '@docs/shared/common/_security.mdx'; + +export const toc = [{}]; + + \ No newline at end of file From 2d3d2fd2a13849e793796395aee716164067c761 Mon Sep 17 00:00:00 2001 From: Kishan Dhakan Date: Tue, 5 Mar 2024 15:50:12 +0530 Subject: [PATCH 5/7] minor changes --- shared/chat-sdk/reference/release-notes/unity.mdx | 2 +- shared/video-sdk/develop/cloud-proxy/reference/react-js.mdx | 3 +++ shared/video-sdk/reference/release-notes/macos.mdx | 4 ++-- shared/video-sdk/reference/release-notes/windows.mdx | 2 +- shared/voice-sdk/reference/release-notes/macos.mdx | 2 +- shared/voice-sdk/reference/release-notes/windows.mdx | 2 +- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/shared/chat-sdk/reference/release-notes/unity.mdx b/shared/chat-sdk/reference/release-notes/unity.mdx index b0747a297..56373d791 100644 --- a/shared/chat-sdk/reference/release-notes/unity.mdx +++ b/shared/chat-sdk/reference/release-notes/unity.mdx @@ -270,4 +270,4 @@ Refer to the following documentations to enable Chat and use the Chat SDK to imp - [Chat Room](../reference/chatroom-overview) - [API Reference](link) - \ No newline at end of file + diff --git a/shared/video-sdk/develop/cloud-proxy/reference/react-js.mdx b/shared/video-sdk/develop/cloud-proxy/reference/react-js.mdx index 337e045a1..77fd3b241 100644 --- a/shared/video-sdk/develop/cloud-proxy/reference/react-js.mdx +++ b/shared/video-sdk/develop/cloud-proxy/reference/react-js.mdx @@ -1,3 +1,6 @@ +- [Reference app](https://github.com/AgoraIO/video-sdk-samples-reactjs/tree/main#samples) +- API reference + diff --git a/shared/video-sdk/reference/release-notes/macos.mdx b/shared/video-sdk/reference/release-notes/macos.mdx index cdc24eb46..185201d7b 100644 --- a/shared/video-sdk/reference/release-notes/macos.mdx +++ b/shared/video-sdk/reference/release-notes/macos.mdx @@ -655,7 +655,7 @@ The SDK supports a new version of AI noise reduction (in comparison to the basic This release adds the following features applicable to spatial audio effect scenarios, which can effectively enhance the user's sense of presence experience in virtual interactive scenarios. -- Sound insulation area: You can set a sound insulation area and sound attenuation parameter by calling `setZones`. When the sound source (which can be a user or the media player) and the listener belong to the inside and outside of the sound insulation area, the listner experiences an attenuation effect similar to that of the sound in the real environment when it encounters a building partition. You can also set the sound attenuation parameter for the media player and the user, respectively, by calling `setPlayerAttenuation` and `setRemoteAudioAttenuation`, and specify whether to use that setting to force an override of the sound attenuation parameter in `setZones`. +- Sound insulation area: You can set a sound insulation area and sound attenuation parameter by calling `setZones`. When the sound source (which can be a user or the media player) and the listener belong to the inside and outside of the sound insulation area, the listner experiences an attenuation effect similar to that of the sound in the real environment when it encounters a building partition. You can also set the sound attenuation parameter for the media player and the user, respectively, by calling `setPlayerAttenuation` and `setRemoteAudioAttenuation`, and specify whether to use that setting to force an override of the sound attenuation paramter in `setZones`. - Doppler sound: You can enable Doppler sound by setting the `enable_doppler` parameter in `SpatialAudioParams`, and the receiver experiences noticeable tonal changes in the event of a high-speed relative displacement between the source source and receiver (such as in a racing game scenario). - Headphone equalizer: You can use a preset headphone equalization effect by calling the `setHeadphoneEQPreset` method to improve the hearing of the headphones. @@ -1000,4 +1000,4 @@ This release improves the implementation logic of `startPreview`. You can call t You can call the `setRemoteDefaultVideoStreamType` method to choose the video stream type when subscribing to streams. - \ No newline at end of file + diff --git a/shared/video-sdk/reference/release-notes/windows.mdx b/shared/video-sdk/reference/release-notes/windows.mdx index eac79c3c5..c83b36bdb 100644 --- a/shared/video-sdk/reference/release-notes/windows.mdx +++ b/shared/video-sdk/reference/release-notes/windows.mdx @@ -1146,4 +1146,4 @@ This release improves the implementation logic of `startPreview`. You can call t **5. Video types of subscription** You can call the `setRemoteDefaultVideoStreamType` method to choose the video stream type when subscribing to streams. - \ No newline at end of file + diff --git a/shared/voice-sdk/reference/release-notes/macos.mdx b/shared/voice-sdk/reference/release-notes/macos.mdx index d472a9d83..415edc476 100644 --- a/shared/voice-sdk/reference/release-notes/macos.mdx +++ b/shared/voice-sdk/reference/release-notes/macos.mdx @@ -657,4 +657,4 @@ This release can achieve the same switching speed as `switchChannelByToken` in v **2. Voice pitch of the local user** This release adds `voicePitch` in `AgoraRtcAudioVolumeInfo` of `reportAudioVolumeIndicationOfSpeakers`. You can use `voicePitch` to get the local user's voice pitch and perform business functions such as rating for singing. - \ No newline at end of file + diff --git a/shared/voice-sdk/reference/release-notes/windows.mdx b/shared/voice-sdk/reference/release-notes/windows.mdx index e61ee33ec..ddbc88846 100644 --- a/shared/voice-sdk/reference/release-notes/windows.mdx +++ b/shared/voice-sdk/reference/release-notes/windows.mdx @@ -744,4 +744,4 @@ This release can achieve the same switching speed as `SwitchChannel` in v3.7.x t **2. Voice pitch of the local user** This release adds `voicePitch` in `AudioVolumeInfo` of `onAudioVolumeIndication`. You can use `voicePitch` to get the local user's voice pitch and perform business functions such as rating for singing. - \ No newline at end of file + From 715387a517f6481c8569e15552d7ce54cdbcdeff Mon Sep 17 00:00:00 2001 From: Kishan Dhakan Date: Tue, 5 Mar 2024 15:52:17 +0530 Subject: [PATCH 6/7] fix typos --- shared/video-sdk/reference/release-notes/android.mdx | 2 +- shared/video-sdk/reference/release-notes/ios.mdx | 2 +- shared/video-sdk/reference/release-notes/macos.mdx | 2 +- shared/video-sdk/reference/release-notes/windows.mdx | 2 +- .../get-started-sdk/project-implementation/electron.mdx | 4 ++-- shared/voice-sdk/reference/release-notes/android.mdx | 2 +- shared/voice-sdk/reference/release-notes/ios.mdx | 2 +- shared/voice-sdk/reference/release-notes/macos.mdx | 2 +- shared/voice-sdk/reference/release-notes/windows.mdx | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/shared/video-sdk/reference/release-notes/android.mdx b/shared/video-sdk/reference/release-notes/android.mdx index c56a50123..d40875a0f 100644 --- a/shared/video-sdk/reference/release-notes/android.mdx +++ b/shared/video-sdk/reference/release-notes/android.mdx @@ -696,7 +696,7 @@ The SDK supports a new version of Noise Suppression (in comparison to the basic This release adds the following features applicable to spatial audio effect scenarios, which can effectively enhance the user's sense of presence experience in virtual interactive scenarios. -- Sound insulation area: You can set a sound insulation area and sound attenuation parameter by calling `setZones`. When the sound source (which can be a user or the media player) and the listener belong to the inside and outside of the sound insulation area, the listner experiences an attenuation effect similar to that of the sound in the real environment when it encounters a building partition. You can also set the sound attenuation parameter for the media player and the user, respectively, by calling `setPlayerAttenuation` and `setRemoteAudioAttenuation`, and specify whether to use that setting to force an override of the sound attenuation paramter in `setZones`. +- Sound insulation area: You can set a sound insulation area and sound attenuation parameter by calling `setZones`. When the sound source (which can be a user or the media player) and the listener belong to the inside and outside of the sound insulation area, the listner experiences an attenuation effect similar to that of the sound in the real environment when it encounters a building partition. You can also set the sound attenuation parameter for the media player and the user, respectively, by calling `setPlayerAttenuation` and `setRemoteAudioAttenuation`, and specify whether to use that setting to force an override of the sound attenuation parameter in `setZones`. - Doppler sound: You can enable Doppler sound by setting the `enable_doppler` parameter in `SpatialAudioParams`, and the receiver experiences noticeable tonal changes in the event of a high-speed relative displacement between the source source and receiver (such as in a racing game scenario). - Headphone equalizer: You can use a preset headphone equalization effect by calling the `setHeadphoneEQPreset` method to improve the hearing of the headphones. diff --git a/shared/video-sdk/reference/release-notes/ios.mdx b/shared/video-sdk/reference/release-notes/ios.mdx index 315ea9aa6..d3b0f7467 100644 --- a/shared/video-sdk/reference/release-notes/ios.mdx +++ b/shared/video-sdk/reference/release-notes/ios.mdx @@ -1205,7 +1205,7 @@ The SDK supports a new version of AI noise reduction (in comparison to the basic This release adds the following features applicable to spatial audio effect scenarios, which can effectively enhance the user's sense of presence experience in virtual interactive scenarios. -- Sound insulation area: You can set a sound insulation area and sound attenuation parameter by calling `setZones`. When the sound source (which can be a user or the media player) and the listener belong to the inside and outside of the sound insulation area, the listner experiences an attenuation effect similar to that of the sound in the real environment when it encounters a building partition. You can also set the sound attenuation parameter for the media player and the user, respectively, by calling `setPlayerAttenuation` and `setRemoteAudioAttenuation`, and specify whether to use that setting to force an override of the sound attenuation paramter in `setZones`. +- Sound insulation area: You can set a sound insulation area and sound attenuation parameter by calling `setZones`. When the sound source (which can be a user or the media player) and the listener belong to the inside and outside of the sound insulation area, the listner experiences an attenuation effect similar to that of the sound in the real environment when it encounters a building partition. You can also set the sound attenuation parameter for the media player and the user, respectively, by calling `setPlayerAttenuation` and `setRemoteAudioAttenuation`, and specify whether to use that setting to force an override of the sound attenuation parameter in `setZones`. - Doppler sound: You can enable Doppler sound by setting the `enable_doppler` parameter in `SpatialAudioParams`, and the receiver experiences noticeable tonal changes in the event of a high-speed relative displacement between the source source and receiver (such as in a racing game scenario). - Headphone equalizer: You can use a preset headphone equalization effect by calling the `setHeadphoneEQPreset` method to improve the hearing of the headphones. diff --git a/shared/video-sdk/reference/release-notes/macos.mdx b/shared/video-sdk/reference/release-notes/macos.mdx index 185201d7b..13b6ee101 100644 --- a/shared/video-sdk/reference/release-notes/macos.mdx +++ b/shared/video-sdk/reference/release-notes/macos.mdx @@ -655,7 +655,7 @@ The SDK supports a new version of AI noise reduction (in comparison to the basic This release adds the following features applicable to spatial audio effect scenarios, which can effectively enhance the user's sense of presence experience in virtual interactive scenarios. -- Sound insulation area: You can set a sound insulation area and sound attenuation parameter by calling `setZones`. When the sound source (which can be a user or the media player) and the listener belong to the inside and outside of the sound insulation area, the listner experiences an attenuation effect similar to that of the sound in the real environment when it encounters a building partition. You can also set the sound attenuation parameter for the media player and the user, respectively, by calling `setPlayerAttenuation` and `setRemoteAudioAttenuation`, and specify whether to use that setting to force an override of the sound attenuation paramter in `setZones`. +- Sound insulation area: You can set a sound insulation area and sound attenuation parameter by calling `setZones`. When the sound source (which can be a user or the media player) and the listener belong to the inside and outside of the sound insulation area, the listner experiences an attenuation effect similar to that of the sound in the real environment when it encounters a building partition. You can also set the sound attenuation parameter for the media player and the user, respectively, by calling `setPlayerAttenuation` and `setRemoteAudioAttenuation`, and specify whether to use that setting to force an override of the sound attenuation parameter in `setZones`. - Doppler sound: You can enable Doppler sound by setting the `enable_doppler` parameter in `SpatialAudioParams`, and the receiver experiences noticeable tonal changes in the event of a high-speed relative displacement between the source source and receiver (such as in a racing game scenario). - Headphone equalizer: You can use a preset headphone equalization effect by calling the `setHeadphoneEQPreset` method to improve the hearing of the headphones. diff --git a/shared/video-sdk/reference/release-notes/windows.mdx b/shared/video-sdk/reference/release-notes/windows.mdx index c83b36bdb..9e8c3fb5a 100644 --- a/shared/video-sdk/reference/release-notes/windows.mdx +++ b/shared/video-sdk/reference/release-notes/windows.mdx @@ -718,7 +718,7 @@ To experience this feature, contact support@a This release adds the following features applicable to spatial audio effect scenarios, which can effectively enhance the user's sense of presence experience in virtual interactive scenarios. -- Sound insulation area: You can set a sound insulation area and sound attenuation parameter by calling `setZones`. When the sound source (which can be a user or the media player) and the listener belong to the inside and outside of the sound insulation area, the listner experiences an attenuation effect similar to that of the sound in the real environment when it encounters a building partition. You can also set the sound attenuation parameter for the media player and the user, respectively, by calling `setPlayerAttenuation` and `setRemoteAudioAttenuation`, and specify whether to use that setting to force an override of the sound attenuation paramter in `setZones`. +- Sound insulation area: You can set a sound insulation area and sound attenuation parameter by calling `setZones`. When the sound source (which can be a user or the media player) and the listener belong to the inside and outside of the sound insulation area, the listner experiences an attenuation effect similar to that of the sound in the real environment when it encounters a building partition. You can also set the sound attenuation parameter for the media player and the user, respectively, by calling `setPlayerAttenuation` and `setRemoteAudioAttenuation`, and specify whether to use that setting to force an override of the sound attenuation parameter in `setZones`. - Doppler sound: You can enable Doppler sound by setting the `enable_doppler` parameter in `SpatialAudioParams`, and the receiver experiences noticeable tonal changes in the event of a high-speed relative displacement between the source source and receiver (such as in a racing game scenario). - Headphone equalizer: You can use a preset headphone equalization effect by calling the `setHeadphoneEQPreset` method to improve the hearing of the headphones. diff --git a/shared/voice-sdk/get-started/get-started-sdk/project-implementation/electron.mdx b/shared/voice-sdk/get-started/get-started-sdk/project-implementation/electron.mdx index 6c8df2507..2da085ad2 100644 --- a/shared/voice-sdk/get-started/get-started-sdk/project-implementation/electron.mdx +++ b/shared/voice-sdk/get-started/get-started-sdk/project-implementation/electron.mdx @@ -50,7 +50,7 @@ To implement this logic, you take the following steps: 2. Call `initialize` to initialize the created instance with your `appID`. -3. To connect to a channel, call `joinChannel` and pass the user ID, token, and channel name as paramters. +3. To connect to a channel, call `joinChannel` and pass the user ID, token, and channel name as parameters. 4. Call `leaveChannel` to leave the channel when the user presses **Leave**. @@ -114,4 +114,4 @@ const } }; ``` - \ No newline at end of file + diff --git a/shared/voice-sdk/reference/release-notes/android.mdx b/shared/voice-sdk/reference/release-notes/android.mdx index a401d50f8..419bfbab6 100644 --- a/shared/voice-sdk/reference/release-notes/android.mdx +++ b/shared/voice-sdk/reference/release-notes/android.mdx @@ -553,7 +553,7 @@ To enable this function, contact [support@agora.io](mailto:support@agora.io). This release adds the following features applicable to spatial audio effect scenarios, which can effectively enhance the user's sense of presence experience in virtual interactive scenarios. -- Sound insulation area: You can set a sound insulation area and sound attenuation parameter by calling `setZones`. When the sound source (which can be a user or the media player) and the listener belong to the inside and outside of the sound insulation area, the listner experiences an attenuation effect similar to that of the sound in the real environment when it encounters a building partition. You can also set the sound attenuation parameter for the media player and the user, respectively, by calling `setPlayerAttenuation` and `setRemoteAudioAttenuation`, and specify whether to use that setting to force an override of the sound attenuation paramter in `setZones`. +- Sound insulation area: You can set a sound insulation area and sound attenuation parameter by calling `setZones`. When the sound source (which can be a user or the media player) and the listener belong to the inside and outside of the sound insulation area, the listner experiences an attenuation effect similar to that of the sound in the real environment when it encounters a building partition. You can also set the sound attenuation parameter for the media player and the user, respectively, by calling `setPlayerAttenuation` and `setRemoteAudioAttenuation`, and specify whether to use that setting to force an override of the sound attenuation parameter in `setZones`. - Doppler sound: You can enable Doppler sound by setting the `enable_doppler` parameter in `SpatialAudioParams`, and the receiver experiences noticeable tonal changes in the event of a high-speed relative displacement between the source source and receiver (such as in a racing game scenario). - Headphone equalizer: You can use a preset headphone equalization effect by calling the `setHeadphoneEQPreset` method to improve the hearing of the headphones. diff --git a/shared/voice-sdk/reference/release-notes/ios.mdx b/shared/voice-sdk/reference/release-notes/ios.mdx index 6c7c4334d..6c0bd952d 100644 --- a/shared/voice-sdk/reference/release-notes/ios.mdx +++ b/shared/voice-sdk/reference/release-notes/ios.mdx @@ -541,7 +541,7 @@ To enable this function, contact support@agora.io. This release adds the following features applicable to spatial audio effect scenarios, which can effectively enhance the user's sense of presence experience in virtual interactive scenarios. -- Sound insulation area: You can set a sound insulation area and sound attenuation parameter by calling `setZones`. When the sound source (which can be a user or the media player) and the listener belong to the inside and outside of the sound insulation area, the listner experiences an attenuation effect similar to that of the sound in the real environment when it encounters a building partition. You can also set the sound attenuation parameter for the media player and the user, respectively, by calling `setPlayerAttenuation` and `setRemoteAudioAttenuation`, and specify whether to use that setting to force an override of the sound attenuation paramter in `setZones`. +- Sound insulation area: You can set a sound insulation area and sound attenuation parameter by calling `setZones`. When the sound source (which can be a user or the media player) and the listener belong to the inside and outside of the sound insulation area, the listner experiences an attenuation effect similar to that of the sound in the real environment when it encounters a building partition. You can also set the sound attenuation parameter for the media player and the user, respectively, by calling `setPlayerAttenuation` and `setRemoteAudioAttenuation`, and specify whether to use that setting to force an override of the sound attenuation parameter in `setZones`. - Doppler sound: You can enable Doppler sound by setting the `enable_doppler` parameter in `SpatialAudioParams`, and the receiver experiences noticeable tonal changes in the event of a high-speed relative displacement between the source source and receiver (such as in a racing game scenario). - Headphone equalizer: You can use a preset headphone equalization effect by calling the `setHeadphoneEQPreset` method to improve the hearing of the headphones. diff --git a/shared/voice-sdk/reference/release-notes/macos.mdx b/shared/voice-sdk/reference/release-notes/macos.mdx index 415edc476..8bd7d32c6 100644 --- a/shared/voice-sdk/reference/release-notes/macos.mdx +++ b/shared/voice-sdk/reference/release-notes/macos.mdx @@ -522,7 +522,7 @@ To enable this function, contact support@agora.io. This release adds the following features applicable to spatial audio effect scenarios, which can effectively enhance the user's sense of presence experience in virtual interactive scenarios. -- Sound insulation area: You can set a sound insulation area and sound attenuation parameter by calling `setZones`. When the sound source (which can be a user or the media player) and the listener belong to the inside and outside of the sound insulation area, the listner experiences an attenuation effect similar to that of the sound in the real environment when it encounters a building partition. You can also set the sound attenuation parameter for the media player and the user, respectively, by calling `setPlayerAttenuation` and `setRemoteAudioAttenuation`, and specify whether to use that setting to force an override of the sound attenuation paramter in `setZones`. +- Sound insulation area: You can set a sound insulation area and sound attenuation parameter by calling `setZones`. When the sound source (which can be a user or the media player) and the listener belong to the inside and outside of the sound insulation area, the listner experiences an attenuation effect similar to that of the sound in the real environment when it encounters a building partition. You can also set the sound attenuation parameter for the media player and the user, respectively, by calling `setPlayerAttenuation` and `setRemoteAudioAttenuation`, and specify whether to use that setting to force an override of the sound attenuation parameter in `setZones`. - Doppler sound: You can enable Doppler sound by setting the `enable_doppler` parameter in `SpatialAudioParams`, and the receiver experiences noticeable tonal changes in the event of a high-speed relative displacement between the source source and receiver (such as in a racing game scenario). - Headphone equalizer: You can use a preset headphone equalization effect by calling the `setHeadphoneEQPreset` method to improve the hearing of the headphones. diff --git a/shared/voice-sdk/reference/release-notes/windows.mdx b/shared/voice-sdk/reference/release-notes/windows.mdx index ddbc88846..3b42f510a 100644 --- a/shared/voice-sdk/reference/release-notes/windows.mdx +++ b/shared/voice-sdk/reference/release-notes/windows.mdx @@ -594,7 +594,7 @@ The SDK uses the playback device as the loopback device by default. Since Date: Tue, 5 Mar 2024 12:54:36 +0200 Subject: [PATCH 7/7] release notes synced --- .../develop/integrate/superclarity.mdx | 2 +- .../chat-sdk/reference/release-notes/ios.mdx | 2 +- .../reference/release-notes/react-native.mdx | 11 +- .../chat-sdk/reference/release-notes/web.mdx | 2 +- .../release-notes/android.mdx | 2 +- .../flexible-classroom/release-notes/ios.mdx | 2 +- .../flexible-classroom/release-notes/js.mdx | 37 ++- shared/signaling/release-notes/web.mdx | 16 ++ .../reference/release-notes/android.mdx | 15 +- .../reference/release-notes/electron.mdx | 208 ++++++++++++++++ .../reference/release-notes/flutter.mdx | 235 +++++++++++++++++- .../video-sdk/reference/release-notes/ios.mdx | 7 +- .../reference/release-notes/react-js.mdx | 69 ++++- .../reference/release-notes/react-native.mdx | 214 +++++++++++++++- .../reference/release-notes/unity.mdx | 209 +++++++++++++++- .../video-sdk/reference/release-notes/web.mdx | 6 +- .../reference/release-notes/android.mdx | 3 +- .../reference/release-notes/electron.mdx | 183 ++++++++++++++ .../reference/release-notes/flutter.mdx | 215 +++++++++++++++- .../reference/release-notes/index.mdx | 2 + .../voice-sdk/reference/release-notes/ios.mdx | 3 +- .../reference/release-notes/react-js.mdx | 65 +++++ .../reference/release-notes/react-native.mdx | 186 +++++++++++++- .../reference/release-notes/unity.mdx | 192 +++++++++++++- 24 files changed, 1852 insertions(+), 34 deletions(-) create mode 100644 shared/voice-sdk/reference/release-notes/react-js.mdx diff --git a/extensions-marketplace/develop/integrate/superclarity.mdx b/extensions-marketplace/develop/integrate/superclarity.mdx index 14691db1f..686557bcf 100644 --- a/extensions-marketplace/develop/integrate/superclarity.mdx +++ b/extensions-marketplace/develop/integrate/superclarity.mdx @@ -1,5 +1,5 @@ --- -title: "Super Clarity" +title: "Super Clarity (beta)" sidebar_position: 7 type: docs description: > diff --git a/shared/chat-sdk/reference/release-notes/ios.mdx b/shared/chat-sdk/reference/release-notes/ios.mdx index 90987d7a6..3089a94d9 100644 --- a/shared/chat-sdk/reference/release-notes/ios.mdx +++ b/shared/chat-sdk/reference/release-notes/ios.mdx @@ -16,7 +16,7 @@ v1.2.0 was released on December 6, 2023. - Added the function of pinning a conversation: - `AgoraChatManager#pinConversation`: Pins a conversation. - `AgoraChatManager#getPinnedConversationsFromServerWithCursor`: Retrieves the pinned conversations from the server. -- Added the `AgoraChatManager#getConversationsFromServerWithCursor` method to retrieve the conversation list from the server. +- Added the `AgoraChatManager#getConversationsFromServerWithCursor` method to retrieve the conversation list from the server. Marked `getConversationsFromServer` and `getConversationsFromServerByPage:pageSize:completion:` deprecated. - Added the `AgoraChatManager#getAllConversations:` method to retrieve local conversations in the reverse chronological order of when conversations are active. - Added `AgoraChatFetchServerMessagesOption` as the parameter configuration class for retrieving historical messages from the server. - Added the `AgoraChatManager#fetchMessagesFromServerBy` method to retrieve historical messages of a conversation from the server according to `AgoraChatFetchServerMessagesOption`, the parameter configuration class for retrieving historical messages. diff --git a/shared/chat-sdk/reference/release-notes/react-native.mdx b/shared/chat-sdk/reference/release-notes/react-native.mdx index 41a884bcf..53a674c56 100644 --- a/shared/chat-sdk/reference/release-notes/react-native.mdx +++ b/shared/chat-sdk/reference/release-notes/react-native.mdx @@ -1,5 +1,13 @@ +## v1.2.1 + +v1.2.1 was released on February 2, 2024. + +#### Improvements + +- Updated native Android dependencies to support Android 14 (API34). + ## v1.2.0 v1.2.0 was released on December 6, 2023. @@ -17,8 +25,7 @@ v1.2.0 was released on December 6, 2023. - Added the function of pinning a conversation: - `ChatManager.pinConversation`: Pins a conversation. - `ChatManager.fetchPinnedConversationsFromServerWithCursor`: Retrieves a list of pinned conversations from the server. -- Added the `ChatManager.fetchConversationsFromServerWithCursor` method to retrieve the conversation list from the server. - Marked the `ChatManager.fetchAllConversations` method deprecated. +- Marked the `ChatManager.fetchAllConversations` method deprecated. - Added the `ChatManager.fetchHistoryMessagesByOptions` method to retrieve historical messages of a conversation from the server according to `ChatFetchMessageOptions`, the parameter configuration class for pulling historical messages. - Added `ChatFetchMessageOptions` as the parameter configuration class for pulling historical messages from the server. - Added the function of managing custom attributes of group members: diff --git a/shared/chat-sdk/reference/release-notes/web.mdx b/shared/chat-sdk/reference/release-notes/web.mdx index 62c18e480..8870a8c68 100644 --- a/shared/chat-sdk/reference/release-notes/web.mdx +++ b/shared/chat-sdk/reference/release-notes/web.mdx @@ -16,7 +16,7 @@ v1.2.0 was released on December 6, 2023. - Added the function of pinning a conversation: - `pinConversation`: Pins a conversation. - `getServerPinnedConversations`: Retrieves the pinned conversations from the server. -- Added the `getServerConversations` method to retrieve the conversation list from the server. +- Added the `getServerConversations` method to retrieve the conversation list from the server. Marked `getConversationlist` deprecated. - Added the `searchOptions` parameter object to the `getHistoryMessages` method for pulling historical messages from the server. - Added the function of managing custom attributes of group members: - `setGroupMemberAttributes`: Sets custom attributes of a group member. diff --git a/shared/flexible-classroom/release-notes/android.mdx b/shared/flexible-classroom/release-notes/android.mdx index abcd45534..e3bf102fe 100644 --- a/shared/flexible-classroom/release-notes/android.mdx +++ b/shared/flexible-classroom/release-notes/android.mdx @@ -300,7 +300,7 @@ As of v1.1.5.1, developers can set the latency level of an audience member. By d - Ultra-low latency: The latency from the sender to the receiver is 400 ms to 800 ms. - Low latency: The latency from the sender to the receiver is 1500 ms to 2000 ms. -The charges for low latency and ultra-low latency are different. For details, see the [pricing page](/interactive-live-streaming/reference/pricing). +The charges for low latency and ultra-low latency are different. For details, see the [pricing page](/interactive-live-streaming/overview/pricing). **Support for setting whether students automatically send streams after going onto the "stage"** diff --git a/shared/flexible-classroom/release-notes/ios.mdx b/shared/flexible-classroom/release-notes/ios.mdx index 668616f0c..93af11833 100644 --- a/shared/flexible-classroom/release-notes/ios.mdx +++ b/shared/flexible-classroom/release-notes/ios.mdx @@ -317,7 +317,7 @@ As of v1.1.5, developers can set the latency level of an audience member. By def - Ultra-low latency: The latency from the sender to the receiver is 400 ms to 800 ms. - Low latency: The latency from the sender to the receiver is 1500 ms to 2000 ms. -The charges for low latency and ultra-low latency are different. For details, see the [pricing page](/interactive-live-streaming/reference/pricing). +The charges for low latency and ultra-low latency are different. For details, see the [pricing page](/interactive-live-streaming/overview/pricing). **Support for setting whether students automatically send streams after going onto the "stage" (Android/iOS)** diff --git a/shared/flexible-classroom/release-notes/js.mdx b/shared/flexible-classroom/release-notes/js.mdx index e47acc337..1781cd0f2 100644 --- a/shared/flexible-classroom/release-notes/js.mdx +++ b/shared/flexible-classroom/release-notes/js.mdx @@ -1,3 +1,34 @@ +## v2.9.20 + +v2.9.20 was released on January 17, 2024. + +#### New features + +This release adds a notification which occurs when it takes too long to start the whiteboard. + +#### Improvements + +This release made the following improvements: + +- Upgraded the RTM service to v1.5.1. +- Optimized network connection by enabling RTC and RTM Cloud Proxy configurations. +- Enhanced the experience of searching for a member student during interaction. +- Improved the display format of the time span during which a student is in the classroom. + +#### Issues fixed + +This release fixed the following issues: + +- Users could join a room before the room was created. +- The loudspeaker list was incorrectly displayed when a user plugged in or unplugged an external audio device. +- The AI noise suppression switch was incorrectly displayed. +- Some words in the user interface of the group discussion were incorrect. +- Abnormalities occurred when a user left the discussion group. +- Issues occurred with sound card capturing. +- The callback was not triggered when the user attribute in a lecture hall was modified. +- Occasionally, when a student frequently enabled or disabled video from a mobile device, the teacher could not subscribe to the video of the student. +- Occasionally, the teacher/student could not operate the whiteboard after joining a discussion group. +- An occasional error occurred when uploading courseware, which lead to failure in displaying the courseware list. ## v2.9.0 @@ -7,12 +38,12 @@ v2.9.0 was released on November 22, 2023. #### New features -This version adds the following new features: +This version adds the following new scenarios: - Online classroom scenarios and the introduction of a new UI style and interactive experience that are closer to the usage habits of educational users. See the following documentation: - - [Scene SDK](../develop/customize-ui/customize-ui-scene-sdk) - - [API Reference](ui-scene) + - [FcrUIScene SDK](../develop/customize-ui/customize-ui-scene-sdk) + - [API Reference](../client-api/ui-scene) One-to-one private chat function. This function does not currently support retrieving historical messages from servers on demand. diff --git a/shared/signaling/release-notes/web.mdx b/shared/signaling/release-notes/web.mdx index efcdd11bf..fc251eda0 100644 --- a/shared/signaling/release-notes/web.mdx +++ b/shared/signaling/release-notes/web.mdx @@ -10,6 +10,22 @@ import * as lock from '@docs/shared/signaling/reference/api-ref/shared/_lock.mdx +### v2.1.7 + +v2.1.7 was released on January 22, 2024. + +#### Improvements + +1. Optimized the processing logic of expired user status data when disconnected and reconnected. + +2. Improved the reliability of message transmission in a Stream Channel under weak network conditions. + +#### Fixed issues + +- When calling the `getOnlineUsers` method and setting `includeUserId` to `true`, the parameter `includeState` set to `false` was missing from the `nextPage` return value. +- In the case of randomly subscribing to a Topic, what the user `message` received in the event notification `publisher` was inconsistent with the actual message sender. +- After repeatedly subscribing to the same channel, unsubscribing or leaving the channel failed. + ### v2.1.5 v2.1.5 was released on October 17, 2023 diff --git a/shared/video-sdk/reference/release-notes/android.mdx b/shared/video-sdk/reference/release-notes/android.mdx index d419bd7d6..03890c395 100644 --- a/shared/video-sdk/reference/release-notes/android.mdx +++ b/shared/video-sdk/reference/release-notes/android.mdx @@ -1,10 +1,11 @@ +import KnownIssues from '@docs/shared/video-sdk/reference/known-issues/android.mdx'; If your target platform is Android 12 or higher, add the `android.permission.BLUETOOTH_CONNECT` permission to the `AndroidManifest.xml` file of the Android project to enable the Bluetooth function of the Android system. ### Known issues -See [Known issues](known-issues). + ### v4.3.0 @@ -579,7 +580,7 @@ As of this release, the SDK optimizes the video encoder algorithm and upgrades t Call the `setVideoEncoderConfiguration` method to set the expected video encoding resolution in the video encoding parameters configuration. - +
    The increase in the default resolution affects the aggregate resolution and thus the billing rate. See Pricing.
    #### New features @@ -876,10 +877,10 @@ This release optimizes the trigger logic of `onVideoSizeChanged`, which can also This release fixed the following issues. -1. When calling `setVideoEncoderConfigurationEx` in the channel to increase the resolution of the video, it occasionally failed. +1. When calling `setVideoEncoderConfigurationEx` in the channel to increase the resolution of the video, it occasionally failed. 2. In online meeting scenarios, the local user and the remote user might not hear each other after the local user is interrupted by a call. 3. After calling `setCloudProxy` to set the cloud proxy, calling `joinChannelEx` to join multiple channels failed. -4. When using the Agora media player to play videos, after you play and pause the video, and then call the seek method to specify a new position for playback, the video image might remain unchanged; if you call the resume method to resume playback, the video might be played in a speed faster than the original one. +4. When using the Agora media player to play videos, after you play and pause the video, and then call the seek method to specify a new position for playback, the video image might remain unchanged; if you call the resume method to resume playback, the video might be played in a speed faster than the original one. #### API changes @@ -910,7 +911,7 @@ This release fixed the following issues. - `enableDualStreamModeEx` - + **Deprecated** @@ -918,7 +919,7 @@ This release fixed the following issues. **Deleted** -- `enableDualStreamMode` [2/3] +- `enableDualStreamMode` [2/3] ### v4.0.0 @@ -969,7 +970,7 @@ The UHD resolution (4K, 60 fps) is currently in beta and requires certain device High resolution typically means higher performance consumption. To avoid a decrease in experience due to insufficient device performance, Agora recommends that you enable FHD and UHD video resolutions on devices with better performance. -The increase in the default resolution affects the aggregate resolution and thus the billing rate. See Pricing. +The increase in the default resolution affects the aggregate resolution and thus the billing rate. See Pricing. **3. Agora media player** diff --git a/shared/video-sdk/reference/release-notes/electron.mdx b/shared/video-sdk/reference/release-notes/electron.mdx index ec6170c9f..7ca6d7317 100644 --- a/shared/video-sdk/reference/release-notes/electron.mdx +++ b/shared/video-sdk/reference/release-notes/electron.mdx @@ -2,6 +2,214 @@ import * as data from '@site/data/variables'; +### v4.3.0 + +v4.3.0 was released on February 28, 2024. + +#### Compatibility changes + +This release has optimized the implementation of some functions, involving renaming or deletion of some APIs. To ensure the normal operation of the project, update the code in the app after upgrading to this release. + +1. **Renaming parameters in callbacks** + + In order to make the parameters in some callbacks and the naming of enumerations in enumeration classes easier to understand, the following modifications have been made in this release. Modify the parameter settings in the callbacks after upgrading to this release. + + | Callback | Original parameter name | New parameter name | + | ---------------------------------- | ----------------------- | ----------------------- | + | `onLocalAudioStateChanged` | `error` | `reason` | + | `onLocalVideoStateChanged` | `error` | `reason` | + | `onDirectCdnStreamingStateChanged` | `error` | `reason` | + | `onRtmpStreamingStateChanged` | `errCode` | `reason` | + + | Original enumeration class | New enumeration class | + | ---------------------------- | -------------------------- | + | `LocalAudioStreamError` | `LocalAudioStreamReason` | + | `LocalVideoStreamError` | `LocalVideoStreamReason` | + | `DirectCdnStreamingError` | `DirectCdnStreamingReason` | + | `MediaPlayerError` | `MediaPlayerReason` | + | `RtmpStreamPublishErrorType` | `RtmpStreamPublishReason` | + + **Note:** For specific renaming of enumerations, please refer to [API changes](#api-changes). + +2. **Channel media relay** + + To improve interface usability, this release removes some methods and callbacks for channel media relay. Use the alternative options listed in the table below: + + | Deleted methods and callbacks | Alternative methods and callbacks | + | ----------------------------------------------------- | ---------------------------------- | + | `startChannelMediaRelay`,`updateChannelMediaRelay` | `startOrUpdateChannelMediaRelay` | + | `startChannelMediaRelayEx`,`updateChannelMediaRelayEx` | `startOrUpdateChannelMediaRelayEx` | + | `onChannelMediaRelayEvent` | `onChannelMediaRelayStateChanged` | + +3. **Reasons for local video state changes** + + This release makes the following modifications to the enumerations in the LocalVideoStreamReason class: + + - The value of `LocalVideoStreamReasonScreenCapturePaused` (formerly `LocalVideoStreamReasonScreenCapturePaused`) has been changed from `23` to `28`. + - The value of `LocalVideoStreamReasonScreenCaptureResumed` (formerly `LocalVideoStreamReasonScreenCaptureResumed`) has been changed from `24` to `29`. + - The `LocalVideoStreamReasonCodecNotSupport` enumeration has been changed to `LocalVideoStreamReasonCodecNotSupport`. + +4. **Audio loopback capturing** + + - Before v4.3.0, if you call the disableAudio method to disable the audio module, audio loopback capturing will not be disabled. + - As of v4.3.0, if you call the disableAudio method to disable the audio module, audio loopback capturing will be disabled as well. If you need to enable audio loopback capturing, you need to enable the audio module by calling the enableAudio method and then call enableLoopbackRecording. + +#### New features + +1. **Local preview with multiple views** + + This release supports local preview with simultaneous display of multiple frames, where the videos shown in the frames are positioned at different observation positions along the video link. Examples of usage are as follows: + + 1. Call setupLocalVideo + to set the first view: Set the `position` parameter to `PositionPostCapturerOrigin` (introduced in this release) in `VideoCanvas`. This corresponds to the position after local video capture and before preprocessing. The video observed here does not have preprocessing effects. + 2. Call setupLocalVideo + to set the second view: Set the `position` parameter to `PositionPostCapturer` in `VideoCanvas`, the video observed here has the effect of video preprocessing. + 3. Observe the local preview effect: The first view is the original video of a real person; the second view is the virtual portrait after video preprocessing (including image enhancement, virtual background, and local preview of watermarks) effects. + +2. **Query Device Score** + + This release adds the queryDeviceScore method to query the device's score level to ensure that the user-set parameters do not exceed the device's capabilities. For example, in HD or UHD video scenarios, you can first call this method to query the device's score. If the returned score is low (for example, below 60), you need to lower the video resolution to avoid affecting the video experience. The minimum device score required for different business scenarios is varied. For specific score recommendations, please contact [technical support](mailto:support@agora.io). + +3. **Select different audio tracks for local playback and streaming** + + This release introduces the selectMultiAudioTrack method that allows you to select different audio tracks for local playback and streaming to remote users. For example, in scenarios like online karaoke, the host can choose to play the original sound locally and publish the accompaniment in the channel. Before using this function, you need to open the media file through the openWithMediaSource method and enable this function by setting the `enableMultiAudioTrack` parameter in MediaSource. + +4. **Others** + + This release has passed the test verification of the following APIs and can be applied to the entire series of RTC 4.x SDK. + + - setRemoteSubscribeFallbackOption: Sets fallback option for the subscribed video stream in weak network conditions. + - onRemoteSubscribeFallbackToAudioOnly: Occurs when the subscribed video stream falls back to audio-only stream due to weak network conditions or switches back to the video stream after the network conditions improve. + - setPlaybackDeviceVolume: Sets the volume of the audio playback device. + - getRecordingDeviceVolume: Sets the volume of the audio capturing device. + - setPlayerOptionInInt and setPlayerOptionInString: Sets media player options for providing technical previews or special customization features. + - enableCustomAudioLocalPlayback: Sets whether to enable the local playback of external audio source. + +#### Improvements + +1. **SDK task processing scheduling optimization** + + This release optimizes the scheduling mechanism for internal tasks within the SDK, with improvements in the following aspects: + + - The speed of video rendering and audio playback for both remote and local first frames improves by 10% to 20%. + - The API call duration and response time are reduced by 5% to 50%. + - The SDK's parallel processing capability significantly improves, delivering higher video quality (720P, 24 fps) even on lower-end devices. Additionally, image processing remains more stable in scenarios involving high resolutions and frame rates. + - The stability of the SDK is further enhanced, leading to a noticeable decrease in the crash rate across various specific scenarios. + +2. **In-ear monitoring volume boost** + + This release provides users with more flexible in-ear monitoring audio adjustment options, supporting the ability to set the in-ear monitoring volume to four times the original volume by calling setInEarMonitoringVolume. + +3. **Spatial audio effects usability improvement** + + - This release optimizes the design of the setZones method, supporting the ability to set the `zones` parameter to `NULL`, indicating the clearing of all echo cancellation zones. + - As of this release, it is no longer necessary to unsubscribe from the audio streams of all remote users within the channel before calling the ILocalSpatialAudioEngine method. + +4. **Other Improvements** + + This release also includes the following improvements: + + - The onLocalVideoStateChanged callback is improved with the inclusion of the `LocalVideoStreamReasonScreenCaptureAutoFallback` enumeration, signaling unexpected errors during the screen sharing process (potentially due to window blocking failure), resulting in performance degradation without impacting the screen sharing process itself. + - This release optimizes the SDK's domain name resolution strategy, improving the stability of calling to resolve domain names in complex network environments. + - When passing in an image with transparent background as the virtual background image, the transparent background can be filled with customized color. + - This release adds the `earMonitorDelay` and `aecEstimatedDelay` members in LocalAudioStats to report ear monitor delay and acoustic echo cancellation (AEC) delay, respectively. + - The onPlayerCacheStats callback is added to report the statistics of the media file being cached. This callback is triggered once per second after file caching is started. + - The onPlayerPlaybackStats callback is added to report the statistics of the media file being played. This callback is triggered once per second after the media file starts playing. You can obtain information like the audio and video bitrate of the media file through PlayerPlaybackStats. + +#### Issues fixed + +This release fixed the following issues: + +- When sharing two screen sharing video streams simultaneously, the reported `captureFrameRate` in the onLocalVideoStats callback is 0, which is not as expected. +- When sharing in a specified screen area, the mouse coordinates within the shared area are inaccurate. When the mouse is near the border of the shared area, the mouse may not be visible in the shared screen. +- The SDK failed to detect any changes in the audio routing after plugging in and out 3.5 mm earphones. + +#### API changes + +**Added** + +- The `subviewUid` member in VideoCanvas +- enableCustomAudioLocalPlayback +- queryDeviceScore +- The `CustomVideoSource` enumeration in MediaSourceType +- The `RouteBluetoothDeviceA2DP` enumeration in AudioRoute +- selectMultiAudioTrack +- onPlayerCacheStats +- onPlayerPlaybackStats +- PlayerPlaybackStats + +**Modified** + +- All `ERROR` fields in the following enumerations are changed to `REASON`: + - `LocalAudioStreamErrorOk` + - `LocalAudioStreamErrorFailure` + - `LocalAudioStreamErrorDeviceNoPermission` + - `LocalAudioStreamErrorDeviceBusy` + - `LocalAudioStreamErrorRecordFailure` + - `LocalAudioStreamErrorEncodeFailure` + - `LocalAudioStreamErrorRecordInvalidId` (Windows) + - `LocalAudioStreamErrorPlayoutInvalidId` (Windows) + - `LocalVideoStreamErrorOk` + - `LocalVideoStreamErrorFailure` + - `LocalVideoStreamErrorDeviceNoPermission` + - `LocalVideoStreamErrorDeviceBusy` + - `LocalVideoStreamErrorCaptureFailure` + - `LocalVideoStreamErrorCodecNotSupport` + - `LocalVideoStreamErrorDeviceNotFound` + - `LocalVideoStreamErrorDeviceDisconnected` + - `LocalVideoStreamErrorDeviceInvalidId` + - `LocalVideoStreamErrorScreenCaptureWindowMinimized` + - `LocalVideoStreamErrorScreenCaptureWindowClosed` + - `LocalVideoStreamErrorScreenCaptureWindowOccluded` + - `DirectCdnStreamingErrorOk` + - `DirectCdnStreamingErrorFailed` + - `DirectCdnStreamingErrorAudioPublication` + - `DirectCdnStreamingErrorVideoPublication` + - `DirectCdnStreamingErrorNetConnect` + - `DirectCdnStreamingErrorBadName` + - `PlayerErrorNone` + - `PlayerErrorInvalidArguments` + - `PlayerErrorInternal` + - `PlayerErrorNoResource` + - `PlayerErrorInvalidMediaSource` + - `PlayerErrorUnknownStreamType` + - `PlayerErrorObjNotInitialized` + - `PlayerErrorCodecNotSupported` + - `PlayerErrorVideoRenderFailed` + - `PlayerErrorInvalidState` + - `PlayerErrorUrlNotFound` + - `PlayerErrorInvalidConnectionState` + - `PlayerErrorSrcBufferUnderflow` + - `PlayerErrorInterrupted` + - `PlayerErrorNotSupported` + - `PlayerErrorTokenExpired` + - `PlayerErrorUnknown` + - `RtmpStreamPublishErrorOk` + - `RtmpStreamPublishErrorInvalidArgument` + - `RtmpStreamPublishErrorEncryptedStreamNotAllowed` + - `RtmpStreamPublishErrorConnectionTimeout` + - `RtmpStreamPublishErrorInternalServerError` + - `RtmpStreamPublishErrorRtmpServerError` + - `RtmpStreamPublishErrorTooOften` + - `RtmpStreamPublishErrorReachLimit` + - `RtmpStreamPublishErrorNotAuthorized` + - `RtmpStreamPublishErrorStreamNotFound` + - `RtmpStreamPublishErrorFormatNotSupported` + - `RtmpStreamPublishErrorNotBroadcaster` + - `RtmpStreamPublishErrorTranscodingNoMixStream` + - `RtmpStreamPublishErrorNetDown` + - `RtmpStreamPublishErrorInvalidPrivilege` + - `RtmpStreamUnpublishErrorOk` + +**Deleted** + +- `startChannelMediaRelay` +- `updateChannelMediaRelay` +- `startChannelMediaRelayEx` +- `updateChannelMediaRelayEx` +- `onChannelMediaRelayEvent` +- `ChannelMediaRelayEvent` + ### v4.2.6 v4.2.6 was released on November 24, 2023. diff --git a/shared/video-sdk/reference/release-notes/flutter.mdx b/shared/video-sdk/reference/release-notes/flutter.mdx index 4a5479e5f..51525f604 100644 --- a/shared/video-sdk/reference/release-notes/flutter.mdx +++ b/shared/video-sdk/reference/release-notes/flutter.mdx @@ -1,8 +1,241 @@ +import KnownIssues from '@docs/shared/video-sdk/reference/known-issues/flutter.mdx'; ### Known issues -See [Known issues](known-issues). + + +### v6.3.0 + +v6.3.0 was released on February 28, 2024. + +#### Compatibility changes + +This release has optimized the implementation of some functions, involving renaming or deletion of some APIs. To ensure the normal operation of the project, you need to update the code in the app after upgrading to this release. + +1. **Renaming parameters in callbacks** + + In order to make the parameters in some callbacks and the naming of enumerations in enumeration classes easier to understand, the following modifications have been made in this release. Please modify the parameter settings in the callbacks after upgrading to this release. + + | Callback | Original parameter name | New parameter name | + | ------------------------------------------------------------ | ----------------------- | ----------------------- | + | `onLocalAudioStateChanged` | `error` | `reason` | + | `onLocalVideoStateChanged` | `error` | `reason` | + | `onDirectCdnStreamingStateChanged` | `error` | `reason` | + | `onRtmpStreamingStateChanged` | `errCode` | `reason` | + + | Original enumeration class | New enumeration class | + | -------------------------- | -------------------------- | + | `LocalAudioStreamError` | `localAudioStreamReason` | + | `LocalVideoStreamError` | `LocalVideoStreamReason` | + | `DirectCdnStreamingError` | `DirectCdnStreamingReason` | + | `MediaPlayerError` | `MediaPlayerReason` | + | `RtmpStreamPublishErrorType` | `RtmpStreamPublishReason` | + + **Note:** For specific renaming of enumerations, please refer to [API changes](#api-changes). + +2. **Channel media relay** + + To improve interface usability, this release removes some methods and callbacks for channel media relay. Use the alternative options listed in the table below: + + | Deleted methods and callbacks | Alternative methods and callbacks | + | ----------------------------------------------------- | ---------------------------------- | + |
    • `startChannelMediaRelay`
    • `updateChannelMediaRelay`
    | `startOrUpdateChannelMediaRelay` | + |
    • `startChannelMediaRelayEx`
    • `updateChannelMediaRelayEx`
    | `startOrUpdateChannelMediaRelayEx` | + | `onChannelMediaRelayEvent` | `onChannelMediaRelayStateChanged` | + +3. **Audio route** + + Starting with this release, `routeBluetooth` in AudioRoute is renamed to `routeHeadsetbluetooth`, representing a Bluetooth device using the HFP protocol. `routeBluetoothSpeaker`(10) is added to represent a Bluetooth device using the A2DP protocol. + +4. **Reasons for local video state changes** + + This release makes the following modifications to the enumerations in the LocalVideoStreamReason class: + + - The value of `localVideoStreamReasonScreenCapturePaused` (formerly `localVideoStreamReasonScreenCapturePaused`) has been changed from 23 to 28. + - The value of `localVideoStreamReasonScreenCaptureResumed` (formerly `localVideoStreamReasonScreenCaptureResumed`) has been changed from 24 to 29. + - The `localVideoStreamErrorEncodeFailure` enumeration has been changed to `localVideoStreamReasonCodecNotSupport`. + +5. **Audio loopback capturing (Windows, macOS)** + + - Before v6.3.0, if you call the disableAudio method to disable the audio module, audio loopback capturing will not be disabled. + - As of v6.3.0, if you call the disableAudio method to disable the audio module, audio loopback capturing will be disabled as well. If you need to enable audio loopback capturing, you need to enable the audio module by calling the enableAudio method and then call enableLoopbackRecording. + +#### New features + +1. **Custom mixed video layout on receiving end (Android, iOS)** + + To facilitate customized layout of mixed video stream at the receiver end, this release introduces the onTranscodedStreamLayoutInfo callback. When the receiver receives the channel's mixed video stream sent by the video mixing server, this callback is triggered, reporting the layout information of the mixed video stream and the layout information of each sub-video stream in the mixed stream. The receiver can set a separate `view` for rendering the sub-video stream (distinguished by `subviewUid`) in the mixed video stream when calling the setupRemoteVideo method, achieving a custom video layout effect. + + When the layout of the sub-video streams in the mixed video stream changes, this callback will also be triggered to report the latest layout information in real time. + + Through this feature, the receiver end can flexibly adjust the local view layout. When applied in a multi-person video scenario, the receiving end only needs to receive and decode a mixed video stream, which can effectively reduce the CPU usage and network bandwidth when decoding multiple video streams on the receiving end. + +2. **Local preview with multiple views** + + This release supports local preview with simultaneous display of multiple frames, where the videos shown in the frames are positioned at different observation positions along the video link. Examples of usage are as follows: + + 1. Call setupLocalVideo to set the first view: Set the `position` parameter to `positionPostCapturerOrigin` (introduced in this release) in `VideoCanvas`. This corresponds to the position after local video capture and before preprocessing. The video observed here does not have preprocessing effects. + 2. Call setupLocalVideo to set the second view: Set the `position` parameter to `positionPostCapturer` in `VideoCanvas`, the video observed here has the effect of video preprocessing. + 3. Observe the local preview effect: The first view is the original video of a real person; the second view is the virtual portrait after video preprocessing (including image enhancement, virtual background, and local preview of watermarks) effects. + +3. **Query device score** + + This release adds the queryDeviceScore method to query the device's score level to ensure that the user-set parameters do not exceed the device's capabilities. For example, in HD or UHD video scenarios, you can first call this method to query the device's score. If the returned score is low (for example, below 60), you need to lower the video resolution to avoid affecting the video experience. The minimum device score required for different business scenarios is varied. For specific score recommendations, please contact [technical support](mailto:support@agora.io). + +4. **Select different audio tracks for local playback and streaming** + + This release introduces the selectMultiAudioTrack method that allows you to select different audio tracks for local playback and streaming to remote users. For example, in scenarios like online karaoke, the host can choose to play the original sound locally and publish the accompaniment in the channel. Before using this function, you need to open the media file through the openWithMediaSource method and enable this function by setting the `enableMultiAudioTrack` parameter in MediaSource. + +5. **Others** + + This release has passed the test verification of the following APIs and can be applied to the entire series of RTC 4.x SDK. + + - setRemoteSubscribeFallbackOption: Sets fallback option for the subscribed video stream in weak network conditions. + - onRemoteSubscribeFallbackToAudioOnly: Occurs when the subscribed video stream falls back to audio-only stream due to weak network conditions or switches back to the video stream after the network conditions improve. + - setPlaybackDeviceVolume (Windows): Sets the volume of the audio playback device. + - setPlaybackDeviceVolume: Sets the volume of the audio capturing device. + - setPlayerOptionInInt and setPlayerOptionInString: Sets media player options for providing technical previews or special customization features. + - enableCustomAudioLocalPlayback: Sets whether to enable the local playback of external audio source. + +#### Improvements + +1. **SDK task processing scheduling optimization** + + This release optimizes the scheduling mechanism for internal tasks within the SDK, with improvements in the following aspects: + + - The speed of video rendering and audio playback for both remote and local first frames improves by 10% to 20%. + - The API call duration and response time are reduced by 5% to 50%. + - The SDK's parallel processing capability significantly improves, delivering higher video quality (720P, 24 fps) even on lower-end devices. Additionally, image processing remains more stable in scenarios involving high resolutions and frame rates. + - The stability of the SDK is further enhanced, leading to a noticeable decrease in the crash rate across various specific scenarios. + +2. **In-ear monitoring volume boost** + + This release provides users with more flexible in-ear monitoring audio adjustment options, supporting the ability to set the in-ear monitoring volume to four times the original volume by calling setInEarMonitoringVolume. + +3. **Spatial audio effects usability improvement** + + - This release optimizes the design of the setZones method, supporting the ability to set the `zones` parameter to `NULL`, indicating the clearing of all echo cancellation zones. + - As of this release, it is no longer necessary to unsubscribe from the audio streams of all remote users within the channel before calling the LocalSpatialAudioEngine method. + +4. **Other Improvements** + + This release also includes the following improvements: + + - The onLocalVideoStateChanged callback is improved with the inclusion of the `localVideoStreamReasonScreenCaptureAutoFallback` enumeration, signaling unexpected errors during the screen sharing process (potentially due to window blocking failure), resulting in performance degradation without impacting the screen sharing process itself (Windows). + - The onPlayerCacheStats callback is added to reports the statistics of the media file being cached. This callback is triggered once per second after file caching is started. + - The onPlayerPlaybackStats callback is added to reports the statistics of the media file being played. This callback is triggered once per second after the media file starts playing. You can obtain information like the audio and video bitrate of the media file through PlayerPlaybackStats. + - This release optimizes the SDK's domain name resolution strategy, improving the stability of calling `setLocalAccessPoint` to resolve domain names in complex network environments. + - When passing in an image with transparent background as the virtual background image, the transparent background can be filled with customized color. + - This release adds the `earMonitorDelay` and `aecEstimatedDelay` members in LocalAudioStats to report ear monitor delay and acoustic echo cancellation (AEC) delay, respectively. + +#### Issues fixed + +This release fixed the following issues: + +- When sharing two screen sharing video streams simultaneously, the reported `captureFrameRate` in the onLocalVideoStats callback is 0, which is not as expected. +- When sharing in a specified screen area, the mouse coordinates within the shared area are inaccurate. When the mouse is near the border of the shared area, the mouse may not be visible in the shared screen (Windows). +- The SDK failed to detect any changes in the audio routing after plugging in and out 3.5mm earphones (Windows). + +#### API changes + +**Added** + +- onTranscodedStreamLayoutInfo (Android, iOS) +- VideoLayout (Android, iOS) +- The `subviewUid` member in VideoCanvas +- enableCustomAudioLocalPlayback +- selectMultiAudioTrack +- onPlayerCacheStats +- onPlayerPlaybackStats +- PlayerPlaybackStats +- The `earMonitorDelay` and `aecEstimatedDelay` members in LocalAudioStats +- queryDeviceScore +- MediaSourceType +- The `routeBluetoothSpeaker` enumeration in AudioRoute + +**Modified** + +- `routeBluetooth` is renamed as `routeHeadsetbluetooth` +- All `Error` fields in the following enumerations are changed to `Reason`: + - `localAudioStreamErrorOk` + - `localAudioStreamErrorFailure` + - `localAudioStreamErrorDeviceNoPermission` + - `localAudioStreamErrorDeviceBusy` + - `localAudioStreamErrorRecordFailure` + - `localAudioStreamErrorEncodeFailure` + - `localAudioStreamErrorRecordInvalidId` (Windows) + - `localAudioStreamErrorPlayoutInvalidId` (Windows) + - `localVideoStreamErrorOk` + - `localVideoStreamErrorFailure` + - `localVideoStreamErrorDeviceNoPermission` + - `localVideoStreamErrorDeviceBusy` + - `localVideoStreamErrorCaptureFailure` + - `localVideoStreamErrorCodecNotSupport` + - `localVideoStreamErrorCaptureInbackground` (iOS) + - `localVideoStreamErrorCaptureMultipleForegroundApps` (iOS) + - `localVideoStreamErrorDeviceNotFound` + - `localVideoStreamErrorDeviceDisconnected` + - `localVideoStreamErrorDeviceInvalidId` + - `localVideoStreamErrorScreenCaptureWindowMinimized` + - `localVideoStreamErrorScreenCaptureWindowClosed` + - `localVideoStreamErrorScreenCaptureWindowOccluded` + - `localVideoStreamErrorScreenCaptureNoPermission` (Windows) + - `localVideoStreamErrorScreenCapturePaused` (Windows) + - `localVideoStreamErrorScreenCaptureResumed` (Windows) + - `localVideoStreamErrorScreenCaptureWindowHidden` (Windows) + - `localVideoStreamErrorScreenCaptureWindowRecoverFromHidden` (Windows) + - `localVideoStreamErrorScreenCaptureWindowRecoverFromMinimized` (Windows) + - `localVideoStreamErrorScreenCaptureFailure` (Windows) + - `localVideoStreamErrorDeviceSystemPressure` (Windows) + - `directCdnStreamingErrorOk` + - `directCdnStreamingErrorFailed` + - `directCdnStreamingErrorAudioPublication` + - `directCdnStreamingErrorVideoPublication` + - `directCdnStreamingErrorNetConnect` + - `directCdnStreamingErrorBadName` + - `playerErrorNone` + - `playerErrorInvalidArguments` + - `playerErrorInternal` + - `playerErrorNoResource` + - `playerErrorInvalidMediaSource` + - `playerErrorUnknownStreamType` + - `playerErrorObjNotInitialized` + - `playerErrorCodecNotSupported` + - `playerErrorVideoRenderFailed` + - `playerErrorInvalidState` + - `playerErrorUrlNotFound` + - `playerErrorInvalidConnectionState` + - `playerErrorSrcBufferUnderflow` + - `playerErrorInterrupted` + - `playerErrorNotSupported` + - `playerErrorTokenExpired` + - `playerErrorUnknown` + - `rtmpStreamPublishErrorOk` + - `rtmpStreamPublishErrorInvalidArgument` + - `rtmpStreamPublishErrorEncryptedStreamNotAllowed` + - `rtmpStreamPublishErrorConnectionTimeout` + - `rtmpStreamPublishErrorInternalServerError` + - `rtmpStreamPublishErrorRtmpServerError` + - `rtmpStreamPublishErrorTooOften` + - `rtmpStreamPublishErrorReachLimit` + - `rtmpStreamPublishErrorNotAuthorized` + - `rtmpStreamPublishErrorStreamNotFound` + - `rtmpStreamPublishErrorFormatNotSupported` + - `rtmpStreamPublishErrorNotBroadcaster` + - `rtmpStreamPublishErrorTranscodingNoMixStream` + - `rtmpStreamPublishErrorNetDown` + - `rtmpStreamPublishErrorInvalidPrivilege` + - `rtmpStreamUnpublishErrorOk` + +**Deleted** + +- `startChannelMediaRelay` +- `updateChannelMediaRelay` +- `startChannelMediaRelayEx` +- `updateChannelMediaRelayEx` +- `onChannelMediaRelayEvent` +- `ChannelMediaRelayEvent` ### v6.2.6 diff --git a/shared/video-sdk/reference/release-notes/ios.mdx b/shared/video-sdk/reference/release-notes/ios.mdx index 281d28b38..228b8607e 100644 --- a/shared/video-sdk/reference/release-notes/ios.mdx +++ b/shared/video-sdk/reference/release-notes/ios.mdx @@ -1,8 +1,9 @@ +import KnownIssues from '@docs/shared/video-sdk/reference/known-issues/ios.mdx'; ### Known issues -See [Known issues](known-issues). + ### v4.3.0 @@ -324,7 +325,7 @@ v4.2.2 was released on July 27, 2023. 1. **Wildcard token** - This release introduces wildcard tokens. Agora supports setting the channel name used for generating a token as a wildcard character. The token generated can be used to join any channel if you use the same user id. In scenarios involving multiple channels, such as switching between different channels, using a wildcard token can avoid repeated application of tokens every time users joining a new channel, which reduces the pressure on your token server. See [Wildcard Tokens](https://docportal.shengwang.cn/cn/live-streaming-premium-4.x/wildcard_token?platform=All%20Platforms). + This release introduces wildcard tokens. Agora supports setting the channel name used for generating a token as a wildcard character. The token generated can be used to join any channel if you use the same user id. In scenarios involving multiple channels, such as switching between different channels, using a wildcard token can avoid repeated application of tokens every time users joining a new channel, which reduces the pressure on your token server. See [Secure authentication with tokens](/en/video-calling/get-started/authentication-workflow#wildcard-tokens).
    All 4.x SDKs support using wildcard tokens.
    @@ -332,7 +333,7 @@ v4.2.2 was released on July 27, 2023. This release adds `preloadChannelByToken [1/2]` and `preloadChannelByToken [2/2]` methods, which allows a user whose role is set as audience to preload channels before joining one. Calling the method can help shortening the time of joining a channel, thus reducing the time it takes for audience members to hear and see the host. - When preloading more than one channels, Agora recommends that you use a wildcard token for preloading to avoid repeated application of tokens every time you joining a new channel, thus saving the time for switching between channels. See [Wildcard Token](https://docportal.shengwang.cn/cn/live-streaming-premium-4.x/wildcard_token?platform=All%20Platforms). + When preloading more than one channels, Agora recommends that you use a wildcard token for preloading to avoid repeated application of tokens every time you joining a new channel, thus saving the time for switching between channels. See [Secure authentication with tokens](/en/video-calling/get-started/authentication-workflow#wildcard-tokens). 3. **Customized background color of video canvas** diff --git a/shared/video-sdk/reference/release-notes/react-js.mdx b/shared/video-sdk/reference/release-notes/react-js.mdx index 97ab97727..20a46a5c7 100644 --- a/shared/video-sdk/reference/release-notes/react-js.mdx +++ b/shared/video-sdk/reference/release-notes/react-js.mdx @@ -1,8 +1,71 @@ -### v2.0.0-alpha.0 +### v2.1.0 -This is the first alpha release of Video SDK for ReactJS. +v2.1.0 was released on January 5, 2024. +This version updates the built-in SDK for Web in the SDK for React JS to version 4.20.0. Check the related changes in the [Web SDK release notes](../overview/release-notes?platform=web#v4200). - \ No newline at end of file +### v2.0.0 + +v2.0.0 was released on December 22, 2023. + +#### Compatibility changes + +**SDK structural optimization** + +The SDK for ReactJS is developed based on the SDK for Web v4.x. To further enhance usability, this version incorporates all Web SDK APIs into the ReactJS SDK, eliminating the need to integrate the Web SDK separately. + +Upon upgrading to this version, make the following modifications: + +- Reintegrate the React SDK. Taking npm as an example: + + ```sh + # Remove existing dependencies + npm uninstall agora-rtc-react agora-rtc-sdk-ng + # Reinstall dependencies + npm install agora-rtc-react + ``` + +- Adjust the import of the `AgoraRTC` interface from the Web SDK. Taking the combined import of `AgoraRTC` and `AgoraRTCProvider` as an example: + + ```jsx + // Before this version + import AgoraRTC from "agora-rtc-sdk-ng"; + import { AgoraRTCProvider } from "agora-rtc-react"; + // As of this version + import AgoraRTC, { AgoraRTCProvider } from "agora-rtc-react" + ``` + +**NetworkQuality renaming** + +To avoid redundancy with the Web SDK's API, this version makes the following changes to the ReactJS SDK's `NetworkQuality` interface: + +- Rename `NetworkQuality` to `NetworkQualityEx` and have `NetworkQualityEx` inherit the `NetworkQuality` interface from the Web SDK. +- Rename the `uplink` and `downlink` properties to `uplinkNetworkQuality` and `downlinkNetworkQuality`, respectively. + +If you use the `NetworkQuality` interface from the ReactJS SDK, make the necessary code modifications after upgrading to this version. + +#### Improvements + +This version adds the `cameraVideoTrackConfig` parameter to `useLocalCameraTrack`, enabling you to set video capture configurations such as capture devices and encoder when creating a camera video track. + +#### Fixed issues + +This version fixed the issue that the SDK threw the error `CAN_NOT_PUBLISH_MULTIPLE_VIDEO_TRACKS` in Firefox's developer mode. + +#### API Changes + +**Added** + +- `cameraVideoTrackConfig` to `useLocalCameraTrack` + +**Modified** + +- `NetworkQuality` to `NetworkQualityEx` + - `uplink` to `uplinkNetworkQuality` + - `downlink` to `downlinkNetworkQuality` + + + +
    diff --git a/shared/video-sdk/reference/release-notes/react-native.mdx b/shared/video-sdk/reference/release-notes/react-native.mdx index c9e5343b1..f96e58ce9 100644 --- a/shared/video-sdk/reference/release-notes/react-native.mdx +++ b/shared/video-sdk/reference/release-notes/react-native.mdx @@ -1,8 +1,220 @@ +import KnownIssues from '@docs/shared/video-sdk/reference/known-issues/react-native.mdx'; ### Known issues -See [Known issues](known-issues). + + +### v4.3.0 + +v4.3.0 was released on February 28, 2024. + +#### Compatibility changes + +This release has optimized the implementation of some functions, involving renaming or deletion of some APIs. To ensure the normal operation of the project, you need to update the code in the app after upgrading to this release. + +1. **Renaming parameters in callbacks** + + In order to make the parameters in some callbacks and the naming of enumerations in enumeration classes easier to understand, the following modifications have been made in this release. Please modify the parameter settings in the callbacks after upgrading to this release. + + | Callback | Original parameter name | New parameter name | + | ---------------------------------- | ----------------------- | ----------------------- | + | `onLocalAudioStateChanged` | `error` | `reason` | + | `onLocalVideoStateChanged` | `error` | `reason` | + | `onDirectCdnStreamingStateChanged` | `error` | `reason` | + | `onRtmpStreamingStateChanged` | `errCode` | `reason` | + + | Original enumeration class | New enumeration class | + | ---------------------------- | -------------------------- | + | `LocalAudioStreamError` | `LocalAudioStreamReason` | + | `LocalVideoStreamError` | `LocalVideoStreamReason` | + | `DirectCdnStreamingError` | `DirectCdnStreamingReason` | + | `MediaPlayerError` | `MediaPlayerReason` | + | `RtmpStreamPublishErrorType` | `RtmpStreamPublishReason` | + + **Note:** For specific renaming of enumerations, please refer to [API changes](#api-changes). + +2. **Channel media relay** + + To improve interface usability, this release removes some methods and callbacks for channel media relay. Use the alternative options listed in the table below: + + | Deleted methods and callbacks | Alternative methods and callbacks | + | ----------------------------------------------------- | ---------------------------------- | + | `startChannelMediaRelay`,`updateChannelMediaRelay` | `startOrUpdateChannelMediaRelay` | + | `startChannelMediaRelayEx`,`updateChannelMediaRelayEx` | `startOrUpdateChannelMediaRelayEx` | + | `onChannelMediaRelayEvent` | `onChannelMediaRelayStateChanged` | + +3. **Audio route** + + Starting with this release, `RouteBluetooth` in AudioRoute is renamed to `RouteBluetoothDeviceHFP`, representing a Bluetooth device using the HFP protocol. `RouteBluetoothDeviceA2DP`(10) is added to represent a Bluetooth device using the A2DP protocol. + +4. **Reasons for local video state changes** + + This release makes the following modifications to the enumerations in the LocalVideoStreamReason class: + + - The value of `LocalVideoStreamReasonScreenCapturePaused` (formerly `LocalVideoStreamReasonScreenCapturePaused`) has been changed from 23 to 28. + - The value of `LocalVideoStreamReasonScreenCaptureResumed` (formerly `LocalVideoStreamReasonScreenCaptureResumed`) has been changed from 24 to 29. + - The `LocalVideoStreamReasonCodecNotSupport` enumeration has been changed to `LocalVideoStreamReasonCodecNotSupport`. + +#### New features + +1. **Custom mixed video layout on receiving end** + + To facilitate customized layout of mixed video stream at the receiver end, this release introduces the onTranscodedStreamLayoutInfo callback. When the receiver receives the channel's mixed video stream sent by the video mixing server, this callback is triggered, reporting the layout information of the mixed video stream and the layout information of each sub-video stream in the mixed stream. The receiver can set a separate `view` for rendering the sub-video stream (distinguished by `subviewUid`) in the mixed video stream when calling the method, achieving a custom video layout effect. + + When the layout of the sub-video streams in the mixed video stream changes, this callback will also be triggered to report the latest layout information in real time. + + Through this feature, the receiver end can flexibly adjust the local view layout. When applied in a multi-person video scenario, the receiving end only needs to receive and decode a mixed video stream, which can effectively reduce the CPU usage and network bandwidth when decoding multiple video streams on the receiving end. + +2. **Local preview with multiple views** + + This release supports local preview with simultaneous display of multiple frames, where the videos shown in the frames are positioned at different observation positions along the video link. Examples of usage are as follows: + + 1. Create the first view: Set the `position` parameter to `PositionPostCapturerOrigin` (introduced in this release) in `VideoCanvas`. This corresponds to the position after local video capture and before preprocessing. The video observed here does not have preprocessing effects. + 2. Create the second view: Set the `position` parameter to `PositionPostCapturer` in `VideoCanvas`, the video observed here has the effect of video preprocessing. + 3. Observe the local preview effect: The first view is the original video of a real person; the second view is the virtual portrait after video preprocessing (including image enhancement, virtual background, and local preview of watermarks) effects. + +3. **Query device score** + + This release adds the queryDeviceScore + method to query the device's score level to ensure that the user-set parameters do not exceed the device's capabilities. For example, in HD or UHD video scenarios, you can first call this method to query the device's score. If the returned score is low (for example, below 60), you need to lower the video resolution to avoid affecting the video experience. The minimum device score required for different business scenarios is varied. For specific score recommendations, please contact [technical support](mailto:support@agora.io). + +4. **Select different audio tracks for local playback and streaming** + + This release introduces the selectMultiAudioTrack method that allows you to select different audio tracks for local playback and streaming to remote users. For example, in scenarios like online karaoke, the host can choose to play the original sound locally and publish the accompaniment in the channel. Before using this function, you need to open the media file through the openWithMediaSource method and enable this function by setting the `enableMultiAudioTrack` parameter in MediaSource. + +5. **Others** + + This release has passed the test verification of the following APIs and can be applied to the entire series of RTC 4.x SDK. + + - setRemoteSubscribeFallbackOption: Sets fallback option for the subscribed video stream in weak network conditions. + - onRemoteSubscribeFallbackToAudioOnly: Occurs when the subscribed video stream falls back to audio-only stream due to weak network conditions or switches back to the video stream after the network conditions improve. + - setPlayerOptionInInt and setPlayerOptionInString: Sets media player options for providing technical previews or special customization features. + - enableCustomAudioLocalPlayback: Sets whether to enable the local playback of external audio source. + +#### Improvements + +1. **SDK task processing scheduling optimization** + + This release optimizes the scheduling mechanism for internal tasks within the SDK, with improvements in the following aspects: + + - The speed of video rendering and audio playback for both remote and local first frames improves by 10% to 20%. + - The API call duration and response time are reduced by 5% to 50%. + - The SDK's parallel processing capability significantly improves, delivering higher video quality (720P, 24 fps) even on lower-end devices. Additionally, image processing remains more stable in scenarios involving high resolutions and frame rates. + - The stability of the SDK is further enhanced, leading to a noticeable decrease in the crash rate across various specific scenarios. + +2. **In-ear monitoring volume boost** + + This release provides users with more flexible in-ear monitoring audio adjustment options, supporting the ability to set the in-ear monitoring volume to four times the original volume by calling setInEarMonitoringVolume. + +3. **Spatial audio effects usability improvement** + + - This release optimizes the design of the setZones method, supporting the ability to set the `zones` parameter to `NULL`, indicating the clearing of all echo cancellation zones. + - As of this release, it is no longer necessary to unsubscribe from the audio streams of all remote users within the channel before calling the ILocalSpatialAudioEngine method. + +4. **Other Improvements** + + This release also includes the following improvements: + + - This release optimizes the SDK's domain name resolution strategy, improving the stability of calling to resolve domain names in complex network environments. + - When passing in an image with transparent background as the virtual background image, the transparent background can be filled with customized color. + - This release adds the `earMonitorDelay` and `aecEstimatedDelay` members in LocalAudioStats to report ear monitor delay and acoustic echo cancellation (AEC) delay, respectively. + - The onPlayerCacheStats callback is added to report the statistics of the media file being cached. This callback is triggered once per second after file caching is started. + - The onPlayerPlaybackStats callback is added to report the statistics of the media file being played. This callback is triggered once per second after the media file starts playing. You can obtain information like the audio and video bitrate of the media file through PlayerPlaybackStats. + +#### Issues fixed + +This release fixed the following issue: + +- When sharing two screen sharing video streams simultaneously, the reported `captureFrameRate` in the onLocalVideoStats callback is 0, which is not as expected. + +#### API changes + +**Added** + +- onTranscodedStreamLayoutInfo +- VideoLayout +- The `subviewUid` member in VideoCanvas +- enableCustomAudioLocalPlayback +- queryDeviceScore +- The `CustomVideoSource` enumeration in MediaSourceType +- The `RouteBluetoothDeviceA2DP` enumeration in AudioRoute +- LocalAudioStats: Adds the `earMonitorDelay` and `aecEstimatedDelay` +- selectMultiAudioTrack +- onPlayerCacheStats +- onPlayerPlaybackStats +- PlayerPlaybackStats + +**Modified** + +- `routeBluetooth` is renamed as`RouteBluetoothDeviceHFP` +- All `ERROR` fields in the following enumerations are changed to `REASON`: + - `LocalAudioStreamErrorOk` + - `LocalAudioStreamErrorFailure` + - `LocalAudioStreamErrorDeviceNoPermission` + - `LocalAudioStreamErrorDeviceBusy` + - `LocalAudioStreamErrorRecordFailure` + - `LocalAudioStreamErrorEncodeFailure` + - `LocalVideoStreamErrorOk` + - `LocalVideoStreamErrorFailure` + - `LocalVideoStreamErrorDeviceNoPermission` + - `LocalVideoStreamErrorDeviceBusy` + - `LocalVideoStreamErrorCaptureFailure` + - `LocalVideoStreamErrorCodecNotSupport` + - `LocalVideoStreamErrorCaptureInbackground` (iOS) + - `LocalVideoStreamErrorCaptureMultipleForegroundApps` (iOS) + - `LocalVideoStreamErrorDeviceNotFound` + - `LocalVideoStreamErrorDeviceDisconnected` + - `LocalVideoStreamErrorDeviceInvalidId` + - `DirectCdnStreamingErrorOk` + - `DirectCdnStreamingErrorFailed` + - `DirectCdnStreamingErrorAudioPublication` + - `DirectCdnStreamingErrorVideoPublication` + - `DirectCdnStreamingErrorNetConnect` + - `DirectCdnStreamingErrorBadName` + - `PlayerErrorNone` + - `PlayerErrorInvalidArguments` + - `PlayerErrorInternal` + - `PlayerErrorNoResource` + - `PlayerErrorInvalidMediaSource` + - `PlayerErrorUnknownStreamType` + - `PlayerErrorObjNotInitialized` + - `PlayerErrorCodecNotSupported` + - `PlayerErrorVideoRenderFailed` + - `PlayerErrorInvalidState` + - `PlayerErrorUrlNotFound` + - `PlayerErrorInvalidConnectionState` + - `PlayerErrorSrcBufferUnderflow` + - `PlayerErrorInterrupted` + - `PlayerErrorNotSupported` + - `PlayerErrorTokenExpired` + - `PlayerErrorUnknown` + - `RtmpStreamPublishErrorOk` + - `RtmpStreamPublishErrorInvalidArgument` + - `RtmpStreamPublishErrorEncryptedStreamNotAllowed` + - `RtmpStreamPublishErrorConnectionTimeout` + - `RtmpStreamPublishErrorInternalServerError` + - `RtmpStreamPublishErrorRtmpServerError` + - `RtmpStreamPublishErrorTooOften` + - `RtmpStreamPublishErrorReachLimit` + - `RtmpStreamPublishErrorNotAuthorized` + - `RtmpStreamPublishErrorStreamNotFound` + - `RtmpStreamPublishErrorFormatNotSupported` + - `RtmpStreamPublishErrorNotBroadcaster` + - `RtmpStreamPublishErrorTranscodingNoMixStream` + - `RtmpStreamPublishErrorNetDown` + - `RtmpStreamPublishErrorInvalidPrivilege` + - `RtmpStreamUnpublishErrorOk` + +**Deleted** + +- `startChannelMediaRelay` +- `updateChannelMediaRelay` +- `startChannelMediaRelayEx` +- `updateChannelMediaRelayEx` +- `onChannelMediaRelayEvent` +- `ChannelMediaRelayEvent` + ### v4.2.6 diff --git a/shared/video-sdk/reference/release-notes/unity.mdx b/shared/video-sdk/reference/release-notes/unity.mdx index 84cdbfa9e..26085d6a8 100644 --- a/shared/video-sdk/reference/release-notes/unity.mdx +++ b/shared/video-sdk/reference/release-notes/unity.mdx @@ -1,8 +1,215 @@ +import KnownIssues from '@docs/shared/video-sdk/reference/known-issues/unity.mdx'; ### Known issues -See [Known issues](known-issues). + + +### v4.3.0 + +v4.3.0 was released on February 28, 2024. + +#### Compatibility changes + +This release has optimized the implementation of some functions, involving renaming or deletion of some APIs. To ensure the normal operation of the project, you need to update the code in the app after upgrading to this release. + +1. **Renaming parameters in callbacks** + + In order to make the parameters in some callbacks and the naming of enumerations in enumeration classes easier to understand, the following modifications have been made in this release. Please modify the parameter settings in the callbacks after upgrading to this release. + + | Callback | Original parameter name | New parameter name | + | ---------------------------------- | ----------------------- | ----------------------- | + | `OnLocalAudioStateChanged` | `error` | `reason` | + | `onLocalVideoStateChanged` | `error` | `reason` | + | `OnDirectCdnStreamingStateChanged` | `error` | `reason` | + | `OnRtmpStreamingStateChanged` | `errCode` | `reason` | + + | Original enumeration class | New enumeration class | + | ---------------------------- | ----------------------------- | + | `LOCAL_AUDIO_STREAM_ERROR` | `LOCAL_AUDIO_STREAM_REASON` | + | `LOCAL_VIDEO_STREAM_ERROR` | `LOCAL_VIDEO_STREAM_REASON` | + | `DIRECT_CDN_STREAMING_ERROR` | `DIRECT_CDN_STREAMING_REASON` | + | `MEDIA_PLAYER_ERROR` | `MEDIA_PLAYER_REASON` | + | `RTMP_STREAM_PUBLISH_ERROR` | `RTMP_STREAM_PUBLISH_REASON` | + + **Note:** For specific renaming of enumerations, please refer to [API changes](#api-changes). + +2. **Channel media relay** + + To improve interface usability, this release removes some methods and callbacks for channel media relay. Use the alternative options listed in the table below: + + | Deleted methods and callbacks | Alternative methods and callbacks | + | ------------------------------------------------------------ | ---------------------------------- | + |
    • `startChannelMediaRelay`
    • `updateChannelMediaRelay`
    | `startOrUpdateChannelMediaRelay` | + |
    • `startChannelMediaRelayEx`
    • `updateChannelMediaRelayEx`
    | `startOrUpdateChannelMediaRelayEx` | + | `onChannelMediaRelayEvent` | `onChannelMediaRelayStateChanged` | + +3. **Reasons for local video state changes** + + This release makes the following modifications to the enumerations in the LOCAL_VIDEO_STREAM_REASON class: + + - The value of `LOCAL_VIDEO_STREAM_REASON_SCREEN_CAPTURE_PAUSED` (formerly `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_PAUSED`) has been changed from 23 to 28. + - The value of `LOCAL_VIDEO_STREAM_REASON_SCREEN_CAPTURE_RESUMED` (formerly `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_RESUMED`) has been changed from 24 to 29. + - The `LOCAL_VIDEO_STREAM_ERROR_CODEC_NOT_SUPPORT` enumeration has been changed to `LOCAL_VIDEO_STREAM_REASON_CODEC_NOT_SUPPORT`. + +#### New features + +1. **Local preview with multiple views** + + This release supports local preview with simultaneous display of multiple frames, where the videos shown in the frames are positioned at different observation positions along the video link. Examples of usage are as follows: + + 1. Call SetupLocalVideo to set the first view: Set the `position` parameter to `POSITION_POST_CAPTURER_ORIGIN` (introduced in this release) in `VideoCanvas`. This corresponds to the position after local video capture and before preprocessing. The video observed here does not have preprocessing effects. + 2. Call SetupLocalVideo to set the second view: Set the `position` parameter to `POSITION_POST_CAPTURER` in `VideoCanvas`, the video observed here has the effect of video preprocessing. + 3. Observe the local preview effect: The first view is the original video of a real person; the second view is the virtual portrait after video preprocessing (including image enhancement, virtual background, and local preview of watermarks) effects. + +2. **Query Device Score** + + This release adds the QueryDeviceScore method to query the device's score level to ensure that the user-set parameters do not exceed the device's capabilities. For example, in HD or UHD video scenarios, you can first call this method to query the device's score. If the returned score is low (for example, below 60), you need to lower the video resolution to avoid affecting the video experience. The minimum device score required for different business scenarios is varied. For specific score recommendations, please contact [technical support](mailto:support@agora.io). + +3. **Select different audio tracks for local playback and streaming** + + This release introduces the SelectMultiAudioTrack method that allows you to select different audio tracks for local playback and streaming to remote users. For example, in scenarios like online karaoke, the host can choose to play the original sound locally and publish the accompaniment in the channel. Before using this function, you need to open the media file through the OpenWithMediaSource method and enable this function by setting the `enableMultiAudioTrack` parameter in MediaSource. + +4. **Others** + + This release has passed the test verification of the following APIs and can be applied to the entire series of RTC 4.x SDK. + + - SetRemoteSubscribeFallbackOption: Sets fallback option for the subscribed video stream in weak network conditions. + - OnRemoteSubscribeFallbackToAudioOnly: Occurs when the subscribed video stream falls back to audio-only stream due to weak network conditions or switches back to the video stream after the network conditions improve. + - GetRecordingDeviceVolume(Windows): Sets the volume of the audio capturing device. + - SetPlayerOption and SetPlayerOption: Sets media player options for providing technical previews or special customization features. + - EnableCustomAudioLocalPlayback: Sets whether to enable the local playback of external audio source. + +#### Improvements + +1. **SDK task processing scheduling optimization** + + This release optimizes the scheduling mechanism for internal tasks within the SDK, with improvements in the following aspects: + + - The speed of video rendering and audio playback for both remote and local first frames improves by 10% to 20%. + - The API call duration and response time are reduced by 5% to 50%. + - The SDK's parallel processing capability significantly improves, delivering higher video quality (720P, 24 fps) even on lower-end devices. Additionally, image processing remains more stable in scenarios involving high resolutions and frame rates. + - The stability of the SDK is further enhanced, leading to a noticeable decrease in the crash rate across various specific scenarios. + +2. **In-ear monitoring volume boost** + + This release provides users with more flexible in-ear monitoring audio adjustment options, supporting the ability to set the in-ear monitoring volume to four times the original volume by calling SetInEarMonitoringVolume. + +3. **Spatial audio effects usability improvement** + + - This release optimizes the design of the SetZones method, supporting the ability to set the `zones` parameter to `NULL`, indicating the clearing of all echo cancellation zones. + - As of this release, it is no longer necessary to unsubscribe from the audio streams of all remote users within the channel before calling the methods in ILocalSpatialAudioEngine. + +4. **Other Improvements** + + This release also includes the following improvements: + + - This release optimizes the SDK's domain name resolution strategy, improving the stability of calling to resolve domain names in complex network environments. + - When passing in an image with transparent background as the virtual background image, the transparent background can be filled with customized color. + - This release adds the `earMonitorDelay` and `aecEstimatedDelay` members in LocalAudioStats to report ear monitor delay and acoustic echo cancellation (AEC) delay, respectively. + - The OnPlayerCacheStats callback is added to report the statistics of the media file being cached. This callback is triggered once per second after file caching is started. + - The OnPlayerPlaybackStats callback is added to report the statistics of the media file being played. This callback is triggered once per second after the media file starts playing. You can obtain information like the audio and video bitrate of the media file through PlayerPlaybackStats. + +#### Issues fixed + +This release fixed the following issues: + +- When sharing two screen sharing video streams simultaneously, the reported `captureFrameRate` in the OnLocalVideoStats callback is 0, which is not as expected. + +#### API changes + +**Added** + +- The `subviewUid` member in VideoCanvas +- EnableCustomAudioLocalPlayback +- QueryDeviceScore +- The `CUSTOM_VIDEO_SOURCE` enumeration in MEDIA_SOURCE_TYPE +- The `ROUTE_BLUETOOTH_DEVICE_A2DP` enumeration in AudioRoute +- SelectMultiAudioTrack +- OnPlayerCacheStats +- OnPlayerPlaybackStats +- PlayerPlaybackStats + +**Modified** + +- All `ERROR` fields in the following enumerations are changed to `REASON`: + + - `LOCAL_AUDIO_STREAM_ERROR_OK` + - `LOCAL_AUDIO_STREAM_ERROR_FAILURE` + - `LOCAL_AUDIO_STREAM_ERROR_DEVICE_NO_PERMISSION` + - `LOCAL_AUDIO_STREAM_ERROR_DEVICE_BUSY` + - `LOCAL_AUDIO_STREAM_ERROR_RECORD_FAILURE` + - `LOCAL_AUDIO_STREAM_ERROR_ENCODE_FAILURE` + - `LOCAL_AUDIO_STREAM_ERROR_RECORD_INVALID_ID` + - `LOCAL_AUDIO_STREAM_ERROR_PLAYOUT_INVALID_ID` + - `LOCAL_VIDEO_STREAM_ERROR_OK` + - `LOCAL_VIDEO_STREAM_ERROR_FAILURE` + - `LOCAL_VIDEO_STREAM_ERROR_DEVICE_NO_PERMISSION` + - `LOCAL_VIDEO_STREAM_ERROR_DEVICE_BUSY` + - `LOCAL_VIDEO_STREAM_ERROR_CAPTURE_FAILURE` + - `LOCAL_VIDEO_STREAM_ERROR_CODEC_NOT_SUPPORT` + - `LOCAL_VIDEO_STREAM_ERROR_DEVICE_NOT_FOUND` + - `LOCAL_VIDEO_STREAM_ERROR_DEVICE_DISCONNECTED` + - `LOCAL_VIDEO_STREAM_ERROR_DEVICE_INVALID_ID` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_MINIMIZED` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_CLOSED` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_OCCLUDED` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_NO_PERMISSION` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_PAUSED` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_RESUMED` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_HIDDEN` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_RECOVER_FROM_HIDDEN` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_RECOVER_FROM_MINIMIZED` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_FAILURE` + - `LOCAL_VIDEO_STREAM_ERROR_DEVICE_SYSTEM_PRESSURE` + - `DIRECT_CDN_STREAMING_ERROR_OK` + - `DIRECT_CDN_STREAMING_ERROR_FAILED` + - `DIRECT_CDN_STREAMING_ERROR_AUDIO_PUBLICATION` + - `DIRECT_CDN_STREAMING_ERROR_VIDEO_PUBLICATION` + - `DIRECT_CDN_STREAMING_ERROR_NET_CONNECT` + - `DIRECT_CDN_STREAMING_ERROR_BAD_NAME` + - `PLAYER_ERROR_NONE` + - `PLAYER_ERROR_INVALID_ARGUMENTS` + - `PLAYER_ERROR_INTERNAL` + - `PLAYER_ERROR_NO_RESOURCE` + - `PLAYER_ERROR_INVALID_MEDIA_SOURCE` + - `PLAYER_ERROR_UNKNOWN_STREAM_TYPE` + - `PLAYER_ERROR_OBJ_NOT_INITIALIZED` + - `PLAYER_ERROR_CODEC_NOT_SUPPORTED` + - `PLAYER_ERROR_VIDEO_RENDER_FAILED` + - `PLAYER_ERROR_INVALID_STATE` + - `PLAYER_ERROR_URL_NOT_FOUND` + - `PLAYER_ERROR_INVALID_CONNECTION_STATE` + - `PLAYER_ERROR_SRC_BUFFER_UNDERFLOW` + - `PLAYER_ERROR_INTERRUPTED` + - `PLAYER_ERROR_NOT_SUPPORTED` + - `PLAYER_ERROR_TOKEN_EXPIRED` + - `PLAYER_ERROR_UNKNOWN` + - `RTMP_STREAM_PUBLISH_ERROR_OK` + - `RTMP_STREAM_PUBLISH_ERROR_INVALID_ARGUMENT` + - `RTMP_STREAM_PUBLISH_ERROR_ENCRYPTED_STREAM_NOT_ALLOWED` + - `RTMP_STREAM_PUBLISH_ERROR_CONNECTION_TIMEOUT` + - `RTMP_STREAM_PUBLISH_ERROR_INTERNAL_SERVER_ERROR` + - `RTMP_STREAM_PUBLISH_ERROR_RTMP_SERVER_ERROR` + - `RTMP_STREAM_PUBLISH_ERROR_TOO_OFTEN` + - `RTMP_STREAM_PUBLISH_ERROR_REACH_LIMIT` + - `RTMP_STREAM_PUBLISH_ERROR_NOT_AUTHORIZED` + - `RTMP_STREAM_PUBLISH_ERROR_STREAM_NOT_FOUND` + - `RTMP_STREAM_PUBLISH_ERROR_FORMAT_NOT_SUPPORTED` + - `RTMP_STREAM_PUBLISH_ERROR_NOT_BROADCASTER` + - `RTMP_STREAM_PUBLISH_ERROR_TRANSCODING_NO_MIX_STREAM` + - `RTMP_STREAM_PUBLISH_ERROR_NET_DOWN` + - `RTMP_STREAM_PUBLISH_ERROR_INVALID_PRIVILEGE` + - `RTMP_STREAM_UNPUBLISH_ERROR_OK` + +**Deleted** + +- `StartChannelMediaRelay` +- `UpdateChannelMediaRelay` +- `StartChannelMediaRelayEx` +- `UpdateChannelMediaRelayEx` +- `OnChannelMediaRelayEvent` +- `CHANNEL_MEDIA_RELAY_EVENT` ### v4.2.6 diff --git a/shared/video-sdk/reference/release-notes/web.mdx b/shared/video-sdk/reference/release-notes/web.mdx index f4053d460..37680dc94 100644 --- a/shared/video-sdk/reference/release-notes/web.mdx +++ b/shared/video-sdk/reference/release-notes/web.mdx @@ -21,7 +21,7 @@ This release adds the following new features: **Send the SEI in the H.264 video streams** - This release introduces the `LocalVideoTrack.sendSeiData` method and the `RemoteVideoTrack.on("sei-received")` event to add and send the Supplemental Enhancement Information (SEI) within the H.264 video streams. The SEI data package has a maximum size of 1KB and encompasses dynamic facial capture data, virtual video information, and digital watermarking. To enable this feature, please contact technical support. + This release introduces the `LocalVideoTrack.sendSeiData` method and the `LocalVideoTrack.sei-received` event to add and send the Supplemental Enhancement Information (SEI) within the H.264 video streams. The SEI data package has a maximum size of 1KB and encompasses dynamic facial capture data, virtual video information, and digital watermarking. To enable this feature, please contact technical support. **Note:** @@ -53,11 +53,13 @@ This release fixes the following issue: **Added**: +- `LocalVideoTrack.sei-received` - `LocalVideoTrack.sendSeiData` -- `RemoteVideoTrack.on("sei-received")` +- `RemoteTrack.video-state-changed` - `RemoteVideoTrack.on("video-state-changed")` - `encryptDataStream` parameter to `AgoraRTCClient.setEncryptionConfig` + ### v4.20.0 v4.20.0 was released on December 8, 2023. diff --git a/shared/voice-sdk/reference/release-notes/android.mdx b/shared/voice-sdk/reference/release-notes/android.mdx index a401d50f8..69a0a591c 100644 --- a/shared/voice-sdk/reference/release-notes/android.mdx +++ b/shared/voice-sdk/reference/release-notes/android.mdx @@ -703,5 +703,4 @@ This release adds `voicePitch` in `AudioVolumeInfo` of `onAudioVolumeIndication` This release adds the `onPermissionError` method, which is automatically reported when the audio capture device or camera does not obtain the appropriate permission. You can enable the corresponding device permission according to the prompt of the callback. -
    - +
    \ No newline at end of file diff --git a/shared/voice-sdk/reference/release-notes/electron.mdx b/shared/voice-sdk/reference/release-notes/electron.mdx index 493119d9c..cd873382f 100644 --- a/shared/voice-sdk/reference/release-notes/electron.mdx +++ b/shared/voice-sdk/reference/release-notes/electron.mdx @@ -1,5 +1,188 @@ +### v4.3.0 + +v4.3.0 was released on February 28, 2024. + +#### Compatibility changes + +This release has optimized the implementation of some functions, involving renaming or deletion of some APIs. To ensure the normal operation of the project, update the code in the app after upgrading to this release. + +1. **Renaming parameters in callbacks** + + In order to make the parameters in some callbacks and the naming of enumerations in enumeration classes easier to understand, the following modifications have been made in this release. Modify the parameter settings in the callbacks after upgrading to this release. + + | Callback | Original parameter name | New parameter name | + | ---------------------------------- | ----------------------- | ----------------------- | + | `onLocalAudioStateChanged` | `error` | `reason` | + | `onLocalVideoStateChanged` | `error` | `reason` | + | `onDirectCdnStreamingStateChanged` | `error` | `reason` | + | `onRtmpStreamingStateChanged` | `errCode` | `reason` | + + | Original enumeration class | New enumeration class | + | ---------------------------- | -------------------------- | + | `LocalAudioStreamError` | `LocalAudioStreamReason` | + | `LocalVideoStreamError` | `LocalVideoStreamReason` | + | `DirectCdnStreamingError` | `DirectCdnStreamingReason` | + | `MediaPlayerError` | `MediaPlayerReason` | + | `RtmpStreamPublishErrorType` | `RtmpStreamPublishReason` | + + **Note:** For specific renaming of enumerations, please refer to [API changes](#api-changes). + +1. **Channel media relay** + + To improve interface usability, this release removes some methods and callbacks for channel media relay. Use the alternative options listed in the table below: + + | Deleted methods and callbacks | Alternative methods and callbacks | + | ----------------------------------------------------- | ---------------------------------- | + | `startChannelMediaRelay`,`updateChannelMediaRelay` | `startOrUpdateChannelMediaRelay` | + | `startChannelMediaRelayEx`,`updateChannelMediaRelayEx` | `startOrUpdateChannelMediaRelayEx` | + | `onChannelMediaRelayEvent` | `onChannelMediaRelayStateChanged` | + +1. **Audio loopback capturing** + + - Before v4.3.0, if you call the disableAudio method to disable the audio module, audio loopback capturing will not be disabled. + - As of v4.3.0, if you call the disableAudio method to disable the audio module, audio loopback capturing will be disabled as well. If you need to enable audio loopback capturing, you need to enable the audio module by calling the enableAudio method and then call enableLoopbackRecording. + +#### New features + +1. **Query Device Score** + + This release adds the queryDeviceScore method to query the device's score level to ensure that the user-set parameters do not exceed the device's capabilities. For example, in HD or UHD video scenarios, you can first call this method to query the device's score. If the returned score is low (for example, below 60), you need to lower the video resolution to avoid affecting the video experience. The minimum device score required for different business scenarios is varied. For specific score recommendations, please contact [technical support](mailto:support@agora.io). + +1. **Select different audio tracks for local playback and streaming** + + This release introduces the selectMultiAudioTrack method that allows you to select different audio tracks for local playback and streaming to remote users. For example, in scenarios like online karaoke, the host can choose to play the original sound locally and publish the accompaniment in the channel. Before using this function, you need to open the media file through the openWithMediaSource method and enable this function by setting the `enableMultiAudioTrack` parameter in MediaSource. + +1. **Others** + + This release has passed the test verification of the following APIs and can be applied to the entire series of RTC 4.x SDK. + + - setPlaybackDeviceVolume: Sets the volume of the audio playback device. + - getRecordingDeviceVolume: Sets the volume of the audio capturing device. + - setPlayerOptionInInt and setPlayerOptionInString: Sets media player options for providing technical previews or special customization features. + - enableCustomAudioLocalPlayback: Sets whether to enable the local playback of external audio source. + +#### Improvements + +1. **SDK task processing scheduling optimization** + + This release optimizes the scheduling mechanism for internal tasks within the SDK, with improvements in the following aspects: + + - The speed of video rendering and audio playback for both remote and local first frames improves by 10% to 20%. + - The API call duration and response time are reduced by 5% to 50%. + - The SDK's parallel processing capability significantly improves, delivering higher video quality (720P, 24 fps) even on lower-end devices. Additionally, image processing remains more stable in scenarios involving high resolutions and frame rates. + - The stability of the SDK is further enhanced, leading to a noticeable decrease in the crash rate across various specific scenarios. + +1. **In-ear monitoring volume boost** + + This release provides users with more flexible in-ear monitoring audio adjustment options, supporting the ability to set the in-ear monitoring volume to four times the original volume by calling setInEarMonitoringVolume. + +1. **Spatial audio effects usability improvement** + + - This release optimizes the design of the setZones method, supporting the ability to set the `zones` parameter to `NULL`, indicating the clearing of all echo cancellation zones. + - As of this release, it is no longer necessary to unsubscribe from the audio streams of all remote users within the channel before calling the ILocalSpatialAudioEngine method. + +1. **Other Improvements** + + This release also includes the following improvements: + + - This release optimizes the SDK's domain name resolution strategy, improving the stability of calling to resolve domain names in complex network environments. + - This release adds the `earMonitorDelay` and `aecEstimatedDelay` members in LocalAudioStats to report ear monitor delay and acoustic echo cancellation (AEC) delay, respectively. + - The onPlayerCacheStats callback is added to report the statistics of the media file being cached. This callback is triggered once per second after file caching is started. + - The onPlayerPlaybackStats callback is added to report the statistics of the media file being played. This callback is triggered once per second after the media file starts playing. You can obtain information like the audio and video bitrate of the media file through PlayerPlaybackStats. + +#### Issues fixed + +This release fixed the following issue: + +- The SDK failed to detect any changes in the audio routing after plugging in and out 3.5 mm earphones. + +#### API changes + +**Added** + +- enableCustomAudioLocalPlayback +- queryDeviceScore +- The `CustomVideoSource` enumeration in MediaSourceType +- The `RouteBluetoothDeviceA2DP` enumeration in AudioRoute +- selectMultiAudioTrack +- onPlayerCacheStats +- onPlayerPlaybackStats +- PlayerPlaybackStats + +**Modified** + +- All `ERROR` fields in the following enumerations are changed to `REASON`: + - `LocalAudioStreamErrorOk` + - `LocalAudioStreamErrorFailure` + - `LocalAudioStreamErrorDeviceNoPermission` + - `LocalAudioStreamErrorDeviceBusy` + - `LocalAudioStreamErrorRecordFailure` + - `LocalAudioStreamErrorEncodeFailure` + - `LocalAudioStreamErrorRecordInvalidId` (Windows) + - `LocalAudioStreamErrorPlayoutInvalidId` (Windows) + - `LocalVideoStreamErrorOk` + - `LocalVideoStreamErrorFailure` + - `LocalVideoStreamErrorDeviceNoPermission` + - `LocalVideoStreamErrorDeviceBusy` + - `LocalVideoStreamErrorCaptureFailure` + - `LocalVideoStreamErrorCodecNotSupport` + - `LocalVideoStreamErrorDeviceNotFound` + - `LocalVideoStreamErrorDeviceDisconnected` + - `LocalVideoStreamErrorDeviceInvalidId` + - `LocalVideoStreamErrorScreenCaptureWindowMinimized` + - `LocalVideoStreamErrorScreenCaptureWindowClosed` + - `LocalVideoStreamErrorScreenCaptureWindowOccluded` + - `DirectCdnStreamingErrorOk` + - `DirectCdnStreamingErrorFailed` + - `DirectCdnStreamingErrorAudioPublication` + - `DirectCdnStreamingErrorVideoPublication` + - `DirectCdnStreamingErrorNetConnect` + - `DirectCdnStreamingErrorBadName` + - `PlayerErrorNone` + - `PlayerErrorInvalidArguments` + - `PlayerErrorInternal` + - `PlayerErrorNoResource` + - `PlayerErrorInvalidMediaSource` + - `PlayerErrorUnknownStreamType` + - `PlayerErrorObjNotInitialized` + - `PlayerErrorCodecNotSupported` + - `PlayerErrorVideoRenderFailed` + - `PlayerErrorInvalidState` + - `PlayerErrorUrlNotFound` + - `PlayerErrorInvalidConnectionState` + - `PlayerErrorSrcBufferUnderflow` + - `PlayerErrorInterrupted` + - `PlayerErrorNotSupported` + - `PlayerErrorTokenExpired` + - `PlayerErrorUnknown` + - `RtmpStreamPublishErrorOk` + - `RtmpStreamPublishErrorInvalidArgument` + - `RtmpStreamPublishErrorEncryptedStreamNotAllowed` + - `RtmpStreamPublishErrorConnectionTimeout` + - `RtmpStreamPublishErrorInternalServerError` + - `RtmpStreamPublishErrorRtmpServerError` + - `RtmpStreamPublishErrorTooOften` + - `RtmpStreamPublishErrorReachLimit` + - `RtmpStreamPublishErrorNotAuthorized` + - `RtmpStreamPublishErrorStreamNotFound` + - `RtmpStreamPublishErrorFormatNotSupported` + - `RtmpStreamPublishErrorNotBroadcaster` + - `RtmpStreamPublishErrorTranscodingNoMixStream` + - `RtmpStreamPublishErrorNetDown` + - `RtmpStreamPublishErrorInvalidPrivilege` + - `RtmpStreamUnpublishErrorOk` + +**Deleted** + +- `startChannelMediaRelay` +- `updateChannelMediaRelay` +- `startChannelMediaRelayEx` +- `updateChannelMediaRelayEx` +- `onChannelMediaRelayEvent` +- `ChannelMediaRelayEvent` + ### v4.2.6 v4.2.6 was released on November 24, 2023. diff --git a/shared/voice-sdk/reference/release-notes/flutter.mdx b/shared/voice-sdk/reference/release-notes/flutter.mdx index 2c25987c6..fc6b67282 100644 --- a/shared/voice-sdk/reference/release-notes/flutter.mdx +++ b/shared/voice-sdk/reference/release-notes/flutter.mdx @@ -1,8 +1,221 @@ +import KnownIssues from '@docs/shared/voice-sdk/reference/known-issues/flutter.mdx'; ### Known issues -See [Known issues](known-issues). + + +### v6.3.0 + +v6.3.0 was released on February 28, 2024. + +#### Compatibility changes + +This release has optimized the implementation of some functions, involving renaming or deletion of some APIs. To ensure the normal operation of the project, you need to update the code in the app after upgrading to this release. + +1. **Renaming parameters in callbacks** + + In order to make the parameters in some callbacks and the naming of enumerations in enumeration classes easier to understand, the following modifications have been made in this release. Please modify the parameter settings in the callbacks after upgrading to this release. + + | Callback | Original parameter name | New parameter name | + | ------------------------------------------------------------ | ----------------------- | ----------------------- | + | `onLocalAudioStateChanged` | `error` | `reason` | + | `onLocalVideoStateChanged` | `error` | `reason` | + | `onDirectCdnStreamingStateChanged` | `error` | `reason` | + | `onRtmpStreamingStateChanged` | `errCode` | `reason` | + + | Original enumeration class | New enumeration class | + | -------------------------- | -------------------------- | + | `LocalAudioStreamError` | `localAudioStreamReason` | + | `LocalVideoStreamError` | `LocalVideoStreamReason` | + | `DirectCdnStreamingError` | `DirectCdnStreamingReason` | + | `MediaPlayerError` | `MediaPlayerReason` | + | `RtmpStreamPublishErrorType` | `RtmpStreamPublishReason` | + + **Note:** For specific renaming of enumerations, please refer to [API changes](#api-changes). + +1. **Channel media relay** + + To improve interface usability, this release removes some methods and callbacks for channel media relay. Use the alternative options listed in the table below: + + | Deleted methods and callbacks | Alternative methods and callbacks | + | ----------------------------------------------------- | ---------------------------------- | + |
    • `startChannelMediaRelay`
    • `updateChannelMediaRelay`
    | `startOrUpdateChannelMediaRelay` | + |
    • `startChannelMediaRelayEx`
    • `updateChannelMediaRelayEx`
    | `startOrUpdateChannelMediaRelayEx` | + | `onChannelMediaRelayEvent` | `onChannelMediaRelayStateChanged` | + +1. **Audio route** + + Starting with this release, `routeBluetooth` in AudioRoute is renamed to `routeHeadsetbluetooth`, representing a Bluetooth device using the HFP protocol. `routeBluetoothSpeaker`(10) is added to represent a Bluetooth device using the A2DP protocol. + + +1. **Audio loopback capturing (Windows, macOS)** + + - Before v6.3.0, if you call the disableAudio method to disable the audio module, audio loopback capturing will not be disabled. + - As of v6.3.0, if you call the disableAudio method to disable the audio module, audio loopback capturing will be disabled as well. If you need to enable audio loopback capturing, you need to enable the audio module by calling the enableAudio method and then call enableLoopbackRecording. + +#### New features + +1. **Local preview with multiple views** + + This release supports local preview with simultaneous display of multiple frames, where the videos shown in the frames are positioned at different observation positions along the video link. Examples of usage are as follows: + + 1. Call setupLocalVideo to set the first view: Set the `position` parameter to `positionPostCapturerOrigin` (introduced in this release) in `VideoCanvas`. This corresponds to the position after local video capture and before preprocessing. The video observed here does not have preprocessing effects. + 1. Call setupLocalVideo to set the second view: Set the `position` parameter to `positionPostCapturer` in `VideoCanvas`, the video observed here has the effect of video preprocessing. + 1. Observe the local preview effect: The first view is the original video of a real person; the second view is the virtual portrait after video preprocessing (including image enhancement, virtual background, and local preview of watermarks) effects. + +1. **Query device score** + + This release adds the queryDeviceScore method to query the device's score level to ensure that the user-set parameters do not exceed the device's capabilities. For example, in HD or UHD video scenarios, you can first call this method to query the device's score. If the returned score is low (for example, below 60), you need to lower the video resolution to avoid affecting the video experience. The minimum device score required for different business scenarios is varied. For specific score recommendations, please contact [technical support](mailto:support@agora.io). + +1. **Select different audio tracks for local playback and streaming** + + This release introduces the selectMultiAudioTrack method that allows you to select different audio tracks for local playback and streaming to remote users. For example, in scenarios like online karaoke, the host can choose to play the original sound locally and publish the accompaniment in the channel. Before using this function, you need to open the media file through the openWithMediaSource method and enable this function by setting the `enableMultiAudioTrack` parameter in MediaSource. + +1. **Others** + + This release has passed the test verification of the following APIs and can be applied to the entire series of RTC 4.x SDK. + + - onRemoteSubscribeFallbackToAudioOnly: Occurs when the subscribed video stream falls back to audio-only stream due to weak network conditions or switches back to the video stream after the network conditions improve. + - setPlaybackDeviceVolume (Windows): Sets the volume of the audio playback device. + - setPlaybackDeviceVolume: Sets the volume of the audio capturing device. + - setPlayerOptionInInt and setPlayerOptionInString: Sets media player options for providing technical previews or special customization features. + - enableCustomAudioLocalPlayback: Sets whether to enable the local playback of external audio source. + +#### Improvements + +1. **SDK task processing scheduling optimization** + + This release optimizes the scheduling mechanism for internal tasks within the SDK, with improvements in the following aspects: + + - The speed of video rendering and audio playback for both remote and local first frames improves by 10% to 20%. + - The API call duration and response time are reduced by 5% to 50%. + - The SDK's parallel processing capability significantly improves, delivering higher video quality (720P, 24 fps) even on lower-end devices. Additionally, image processing remains more stable in scenarios involving high resolutions and frame rates. + - The stability of the SDK is further enhanced, leading to a noticeable decrease in the crash rate across various specific scenarios. + +1. **In-ear monitoring volume boost** + + This release provides users with more flexible in-ear monitoring audio adjustment options, supporting the ability to set the in-ear monitoring volume to four times the original volume by calling setInEarMonitoringVolume. + +1. **Spatial audio effects usability improvement** + + - This release optimizes the design of the setZones method, supporting the ability to set the `zones` parameter to `NULL`, indicating the clearing of all echo cancellation zones. + - As of this release, it is no longer necessary to unsubscribe from the audio streams of all remote users within the channel before calling the LocalSpatialAudioEngine method. + +1. **Other Improvements** + + This release also includes the following improvements: + + - The onPlayerCacheStats callback is added to reports the statistics of the media file being cached. This callback is triggered once per second after file caching is started. + - The onPlayerPlaybackStats callback is added to reports the statistics of the media file being played. This callback is triggered once per second after the media file starts playing. You can obtain information like the audio and video bitrate of the media file through PlayerPlaybackStats. + - This release optimizes the SDK's domain name resolution strategy, improving the stability of calling `setLocalAccessPoint` to resolve domain names in complex network environments. + - This release adds the `earMonitorDelay` and `aecEstimatedDelay` members in LocalAudioStats to report ear monitor delay and acoustic echo cancellation (AEC) delay, respectively. + +#### Issues fixed + +This release fixed the following issue: + +- The SDK failed to detect any changes in the audio routing after plugging in and out 3.5mm earphones (Windows). + +#### API changes + +**Added** + +- onTranscodedStreamLayoutInfo (Android, iOS) +- VideoLayout (Android, iOS) +- The `subviewUid` member in VideoCanvas +- enableCustomAudioLocalPlayback +- selectMultiAudioTrack +- onPlayerCacheStats +- onPlayerPlaybackStats +- PlayerPlaybackStats +- The `earMonitorDelay` and `aecEstimatedDelay` members in LocalAudioStats +- queryDeviceScore +- MediaSourceType +- The `routeBluetoothSpeaker` enumeration in AudioRoute + +**Modified** + +- `routeBluetooth` is renamed as `routeHeadsetbluetooth` +- All `Error` fields in the following enumerations are changed to `Reason`: + - `localAudioStreamErrorOk` + - `localAudioStreamErrorFailure` + - `localAudioStreamErrorDeviceNoPermission` + - `localAudioStreamErrorDeviceBusy` + - `localAudioStreamErrorRecordFailure` + - `localAudioStreamErrorEncodeFailure` + - `localAudioStreamErrorRecordInvalidId` (Windows) + - `localAudioStreamErrorPlayoutInvalidId` (Windows) + - `localVideoStreamErrorOk` + - `localVideoStreamErrorFailure` + - `localVideoStreamErrorDeviceNoPermission` + - `localVideoStreamErrorDeviceBusy` + - `localVideoStreamErrorCaptureFailure` + - `localVideoStreamErrorCodecNotSupport` + - `localVideoStreamErrorCaptureInbackground` (iOS) + - `localVideoStreamErrorCaptureMultipleForegroundApps` (iOS) + - `localVideoStreamErrorDeviceNotFound` + - `localVideoStreamErrorDeviceDisconnected` + - `localVideoStreamErrorDeviceInvalidId` + - `localVideoStreamErrorScreenCaptureWindowMinimized` + - `localVideoStreamErrorScreenCaptureWindowClosed` + - `localVideoStreamErrorScreenCaptureWindowOccluded` + - `localVideoStreamErrorScreenCaptureNoPermission` (Windows) + - `localVideoStreamErrorScreenCapturePaused` (Windows) + - `localVideoStreamErrorScreenCaptureResumed` (Windows) + - `localVideoStreamErrorScreenCaptureWindowHidden` (Windows) + - `localVideoStreamErrorScreenCaptureWindowRecoverFromHidden` (Windows) + - `localVideoStreamErrorScreenCaptureWindowRecoverFromMinimized` (Windows) + - `localVideoStreamErrorScreenCaptureFailure` (Windows) + - `localVideoStreamErrorDeviceSystemPressure` (Windows) + - `directCdnStreamingErrorOk` + - `directCdnStreamingErrorFailed` + - `directCdnStreamingErrorAudioPublication` + - `directCdnStreamingErrorVideoPublication` + - `directCdnStreamingErrorNetConnect` + - `directCdnStreamingErrorBadName` + - `playerErrorNone` + - `playerErrorInvalidArguments` + - `playerErrorInternal` + - `playerErrorNoResource` + - `playerErrorInvalidMediaSource` + - `playerErrorUnknownStreamType` + - `playerErrorObjNotInitialized` + - `playerErrorCodecNotSupported` + - `playerErrorVideoRenderFailed` + - `playerErrorInvalidState` + - `playerErrorUrlNotFound` + - `playerErrorInvalidConnectionState` + - `playerErrorSrcBufferUnderflow` + - `playerErrorInterrupted` + - `playerErrorNotSupported` + - `playerErrorTokenExpired` + - `playerErrorUnknown` + - `rtmpStreamPublishErrorOk` + - `rtmpStreamPublishErrorInvalidArgument` + - `rtmpStreamPublishErrorEncryptedStreamNotAllowed` + - `rtmpStreamPublishErrorConnectionTimeout` + - `rtmpStreamPublishErrorInternalServerError` + - `rtmpStreamPublishErrorRtmpServerError` + - `rtmpStreamPublishErrorTooOften` + - `rtmpStreamPublishErrorReachLimit` + - `rtmpStreamPublishErrorNotAuthorized` + - `rtmpStreamPublishErrorStreamNotFound` + - `rtmpStreamPublishErrorFormatNotSupported` + - `rtmpStreamPublishErrorNotBroadcaster` + - `rtmpStreamPublishErrorTranscodingNoMixStream` + - `rtmpStreamPublishErrorNetDown` + - `rtmpStreamPublishErrorInvalidPrivilege` + - `rtmpStreamUnpublishErrorOk` + +**Deleted** + +- `startChannelMediaRelay` +- `updateChannelMediaRelay` +- `startChannelMediaRelayEx` +- `updateChannelMediaRelayEx` +- `onChannelMediaRelayEvent` +- `ChannelMediaRelayEvent` ### v6.2.6 diff --git a/shared/voice-sdk/reference/release-notes/index.mdx b/shared/voice-sdk/reference/release-notes/index.mdx index 9530ce6f5..303f474b8 100644 --- a/shared/voice-sdk/reference/release-notes/index.mdx +++ b/shared/voice-sdk/reference/release-notes/index.mdx @@ -2,6 +2,7 @@ import Android from './android.mdx'; import Unity from './unity.mdx'; import Flutter from './flutter.mdx'; import ReactNative from './react-native.mdx'; +import ReactJS from './react-js.mdx'; import Electron from './electron.mdx'; import Ios from './ios.mdx'; import Macos from './macos.mdx'; @@ -24,6 +25,7 @@ This page provides the release notes for the +import KnownIssues from '@docs/shared/voice-sdk/reference/known-issues/ios.mdx'; ### Known issues -See [Known issues](known-issues). + ### v4.3.0 diff --git a/shared/voice-sdk/reference/release-notes/react-js.mdx b/shared/voice-sdk/reference/release-notes/react-js.mdx new file mode 100644 index 000000000..5105761d2 --- /dev/null +++ b/shared/voice-sdk/reference/release-notes/react-js.mdx @@ -0,0 +1,65 @@ + + +### v2.0.0 + +v2.0.0 was released on December XX, 2023. + +#### Compatibility changes + +**SDK structural optimization** + +The SDK for ReactJS is developed based on the SDK for Web v4.x. To further enhance usability, this version incorporates all Web SDK APIs into the ReactJS SDK, eliminating the need to integrate the Web SDK separately. + +Upon upgrading to this version, make the following modifications: + +- Reintegrate the React SDK. Taking npm as an example: + + ```sh + # Remove existing dependencies + npm uninstall agora-rtc-react agora-rtc-sdk-ng + # Reinstall dependencies + npm install agora-rtc-react + ``` + +- Adjust the import of the `AgoraRTC` interface from the Web SDK. Taking the combined import of `AgoraRTC` and `AgoraRTCProvider` as an example: + + ```jsx + // Before this version + import AgoraRTC from "agora-rtc-sdk-ng"; + import { AgoraRTCProvider } from "agora-rtc-react"; + // As of this version + import AgoraRTC, { AgoraRTCProvider } from "agora-rtc-react" + ``` + +**NetworkQuality renaming** + +To avoid redundancy with the Web SDK's API, this version makes the following changes to the ReactJS SDK's `NetworkQuality` interface: + +- Rename `NetworkQuality` to `NetworkQualityEx` and have `NetworkQualityEx` inherit the `NetworkQuality` interface from the Web SDK. +- Rename the `uplink` and `downlink` properties to `uplinkNetworkQuality` and `downlinkNetworkQuality`, respectively. + +If you use the `NetworkQuality` interface from the ReactJS SDK, make the necessary code modifications after upgrading to this version. + +#### Improvements + +This version adds the `cameraVideoTrackConfig` parameter to `useLocalCameraTrack`, enabling you to set video capture configurations such as capture devices and encoder when creating a camera video track. + +#### Fixed issues + +This version fixed the issue that the SDK threw the error `CAN_NOT_PUBLISH_MULTIPLE_VIDEO_TRACKS` in Firefox's developer mode. + +#### API Changes + +**Added** + +- `cameraVideoTrackConfig` to `useLocalCameraTrack` + +**Modified** + +- `NetworkQuality` to `NetworkQualityEx` + - `uplink` to `uplinkNetworkQuality` + - `downlink` to `downlinkNetworkQuality` + + + + \ No newline at end of file diff --git a/shared/voice-sdk/reference/release-notes/react-native.mdx b/shared/voice-sdk/reference/release-notes/react-native.mdx index 7933dc321..dd3e187d8 100644 --- a/shared/voice-sdk/reference/release-notes/react-native.mdx +++ b/shared/voice-sdk/reference/release-notes/react-native.mdx @@ -1,8 +1,192 @@ +import KnownIssues from '@docs/shared/voice-sdk/reference/known-issues/react-native.mdx'; ### Known issues -See [Known issues](known-issues). + + +### v4.3.0 + +v4.3.0 was released on February 28, 2024. + +#### Compatibility changes + +This release has optimized the implementation of some functions, involving renaming or deletion of some APIs. To ensure the normal operation of the project, you need to update the code in the app after upgrading to this release. + +1. **Renaming parameters in callbacks** + + In order to make the parameters in some callbacks and the naming of enumerations in enumeration classes easier to understand, the following modifications have been made in this release. Please modify the parameter settings in the callbacks after upgrading to this release. + + | Callback | Original parameter name | New parameter name | + | ---------------------------------- | ----------------------- | ----------------------- | + | `onLocalAudioStateChanged` | `error` | `reason` | + | `onLocalVideoStateChanged` | `error` | `reason` | + | `onDirectCdnStreamingStateChanged` | `error` | `reason` | + | `onRtmpStreamingStateChanged` | `errCode` | `reason` | + + | Original enumeration class | New enumeration class | + | ---------------------------- | -------------------------- | + | `LocalAudioStreamError` | `LocalAudioStreamReason` | + | `LocalVideoStreamError` | `LocalVideoStreamReason` | + | `DirectCdnStreamingError` | `DirectCdnStreamingReason` | + | `MediaPlayerError` | `MediaPlayerReason` | + | `RtmpStreamPublishErrorType` | `RtmpStreamPublishReason` | + + **Note:** For specific renaming of enumerations, please refer to [API changes](#api-changes). + +2. **Channel media relay** + + To improve interface usability, this release removes some methods and callbacks for channel media relay. Use the alternative options listed in the table below: + + | Deleted methods and callbacks | Alternative methods and callbacks | + | ----------------------------------------------------- | ---------------------------------- | + | `startChannelMediaRelay`,`updateChannelMediaRelay` | `startOrUpdateChannelMediaRelay` | + | `startChannelMediaRelayEx`,`updateChannelMediaRelayEx` | `startOrUpdateChannelMediaRelayEx` | + | `onChannelMediaRelayEvent` | `onChannelMediaRelayStateChanged` | + +3. **Audio route** + + Starting with this release, `RouteBluetooth` in AudioRoute is renamed to `RouteBluetoothDeviceHFP`, representing a Bluetooth device using the HFP protocol. `RouteBluetoothDeviceA2DP`(10) is added to represent a Bluetooth device using the A2DP protocol. + +#### New features + +1. **Query device score** + + This release adds the queryDeviceScore + method to query the device's score level to ensure that the user-set parameters do not exceed the device's capabilities. For example, in HD or UHD video scenarios, you can first call this method to query the device's score. If the returned score is low (for example, below 60), you need to lower the video resolution to avoid affecting the video experience. The minimum device score required for different business scenarios is varied. For specific score recommendations, please contact [technical support](mailto:support@agora.io). + +1. **Select different audio tracks for local playback and streaming** + + This release introduces the selectMultiAudioTrack method that allows you to select different audio tracks for local playback and streaming to remote users. For example, in scenarios like online karaoke, the host can choose to play the original sound locally and publish the accompaniment in the channel. Before using this function, you need to open the media file through the openWithMediaSource method and enable this function by setting the `enableMultiAudioTrack` parameter in MediaSource. + +1. **Others** + + This release has passed the test verification of the following APIs and can be applied to the entire series of RTC 4.x SDK. + + - setRemoteSubscribeFallbackOption: Sets fallback option for the subscribed video stream in weak network conditions. + - onRemoteSubscribeFallbackToAudioOnly: Occurs when the subscribed video stream falls back to audio-only stream due to weak network conditions or switches back to the video stream after the network conditions improve. + - setPlayerOptionInInt and setPlayerOptionInString: Sets media player options for providing technical previews or special customization features. + - enableCustomAudioLocalPlayback: Sets whether to enable the local playback of external audio source. + +#### Improvements + +1. **SDK task processing scheduling optimization** + + This release optimizes the scheduling mechanism for internal tasks within the SDK, with improvements in the following aspects: + + - The speed of video rendering and audio playback for both remote and local first frames improves by 10% to 20%. + - The API call duration and response time are reduced by 5% to 50%. + - The SDK's parallel processing capability significantly improves, delivering higher video quality (720P, 24 fps) even on lower-end devices. Additionally, image processing remains more stable in scenarios involving high resolutions and frame rates. + - The stability of the SDK is further enhanced, leading to a noticeable decrease in the crash rate across various specific scenarios. + +1. **In-ear monitoring volume boost** + + This release provides users with more flexible in-ear monitoring audio adjustment options, supporting the ability to set the in-ear monitoring volume to four times the original volume by calling setInEarMonitoringVolume. + +1. **Spatial audio effects usability improvement** + + - This release optimizes the design of the setZones method, supporting the ability to set the `zones` parameter to `NULL`, indicating the clearing of all echo cancellation zones. + - As of this release, it is no longer necessary to unsubscribe from the audio streams of all remote users within the channel before calling the ILocalSpatialAudioEngine method. + +1. **Other Improvements** + + This release also includes the following improvements: + + - This release optimizes the SDK's domain name resolution strategy, improving the stability of calling to resolve domain names in complex network environments. + - When passing in an image with transparent background as the virtual background image, the transparent background can be filled with customized color. + - This release adds the `earMonitorDelay` and `aecEstimatedDelay` members in LocalAudioStats to report ear monitor delay and acoustic echo cancellation (AEC) delay, respectively. + - The onPlayerCacheStats callback is added to report the statistics of the media file being cached. This callback is triggered once per second after file caching is started. + - The onPlayerPlaybackStats callback is added to report the statistics of the media file being played. This callback is triggered once per second after the media file starts playing. You can obtain information like the audio and video bitrate of the media file through PlayerPlaybackStats. + +#### Issues fixed + +This release fixed the following issue: + +- When sharing two screen sharing video streams simultaneously, the reported `captureFrameRate` in the onLocalVideoStats callback is 0, which is not as expected. + +#### API changes + +**Added** + +- enableCustomAudioLocalPlayback +- queryDeviceScore +- The `CustomVideoSource` enumeration in MediaSourceType +- The `RouteBluetoothDeviceA2DP` enumeration in AudioRoute +- LocalAudioStats: Adds the `earMonitorDelay` and `aecEstimatedDelay` +- selectMultiAudioTrack +- onPlayerCacheStats +- onPlayerPlaybackStats +- PlayerPlaybackStats + +**Modified** + +- `routeBluetooth` is renamed as`RouteBluetoothDeviceHFP` +- All `ERROR` fields in the following enumerations are changed to `REASON`: + - `LocalAudioStreamErrorOk` + - `LocalAudioStreamErrorFailure` + - `LocalAudioStreamErrorDeviceNoPermission` + - `LocalAudioStreamErrorDeviceBusy` + - `LocalAudioStreamErrorRecordFailure` + - `LocalAudioStreamErrorEncodeFailure` + - `LocalVideoStreamErrorOk` + - `LocalVideoStreamErrorFailure` + - `LocalVideoStreamErrorDeviceNoPermission` + - `LocalVideoStreamErrorDeviceBusy` + - `LocalVideoStreamErrorCaptureFailure` + - `LocalVideoStreamErrorCodecNotSupport` + - `LocalVideoStreamErrorCaptureInbackground` (iOS) + - `LocalVideoStreamErrorCaptureMultipleForegroundApps` (iOS) + - `LocalVideoStreamErrorDeviceNotFound` + - `LocalVideoStreamErrorDeviceDisconnected` + - `LocalVideoStreamErrorDeviceInvalidId` + - `DirectCdnStreamingErrorOk` + - `DirectCdnStreamingErrorFailed` + - `DirectCdnStreamingErrorAudioPublication` + - `DirectCdnStreamingErrorVideoPublication` + - `DirectCdnStreamingErrorNetConnect` + - `DirectCdnStreamingErrorBadName` + - `PlayerErrorNone` + - `PlayerErrorInvalidArguments` + - `PlayerErrorInternal` + - `PlayerErrorNoResource` + - `PlayerErrorInvalidMediaSource` + - `PlayerErrorUnknownStreamType` + - `PlayerErrorObjNotInitialized` + - `PlayerErrorCodecNotSupported` + - `PlayerErrorVideoRenderFailed` + - `PlayerErrorInvalidState` + - `PlayerErrorUrlNotFound` + - `PlayerErrorInvalidConnectionState` + - `PlayerErrorSrcBufferUnderflow` + - `PlayerErrorInterrupted` + - `PlayerErrorNotSupported` + - `PlayerErrorTokenExpired` + - `PlayerErrorUnknown` + - `RtmpStreamPublishErrorOk` + - `RtmpStreamPublishErrorInvalidArgument` + - `RtmpStreamPublishErrorEncryptedStreamNotAllowed` + - `RtmpStreamPublishErrorConnectionTimeout` + - `RtmpStreamPublishErrorInternalServerError` + - `RtmpStreamPublishErrorRtmpServerError` + - `RtmpStreamPublishErrorTooOften` + - `RtmpStreamPublishErrorReachLimit` + - `RtmpStreamPublishErrorNotAuthorized` + - `RtmpStreamPublishErrorStreamNotFound` + - `RtmpStreamPublishErrorFormatNotSupported` + - `RtmpStreamPublishErrorNotBroadcaster` + - `RtmpStreamPublishErrorTranscodingNoMixStream` + - `RtmpStreamPublishErrorNetDown` + - `RtmpStreamPublishErrorInvalidPrivilege` + - `RtmpStreamUnpublishErrorOk` + +**Deleted** + +- `startChannelMediaRelay` +- `updateChannelMediaRelay` +- `startChannelMediaRelayEx` +- `updateChannelMediaRelayEx` +- `onChannelMediaRelayEvent` +- `ChannelMediaRelayEvent` ### v4.2.6 diff --git a/shared/voice-sdk/reference/release-notes/unity.mdx b/shared/voice-sdk/reference/release-notes/unity.mdx index ead7a1fff..d9a6cfacf 100644 --- a/shared/voice-sdk/reference/release-notes/unity.mdx +++ b/shared/voice-sdk/reference/release-notes/unity.mdx @@ -1,8 +1,198 @@ +import KnownIssues from '@docs/shared/voice-sdk/reference/known-issues/unity.mdx'; ### Known issues -See [Known issues](known-issues). + + +### v4.3.0 + +v4.3.0 was released on February 28, 2024. + +#### Compatibility changes + +This release has optimized the implementation of some functions, involving renaming or deletion of some APIs. To ensure the normal operation of the project, you need to update the code in the app after upgrading to this release. + +1. **Renaming parameters in callbacks** + + In order to make the parameters in some callbacks and the naming of enumerations in enumeration classes easier to understand, the following modifications have been made in this release. Please modify the parameter settings in the callbacks after upgrading to this release. + + | Callback | Original parameter name | New parameter name | + | ---------------------------------- | ----------------------- | ----------------------- | + | `OnLocalAudioStateChanged` | `error` | `reason` | + | `onLocalVideoStateChanged` | `error` | `reason` | + | `OnDirectCdnStreamingStateChanged` | `error` | `reason` | + | `OnRtmpStreamingStateChanged` | `errCode` | `reason` | + + | Original enumeration class | New enumeration class | + | ---------------------------- | ----------------------------- | + | `LOCAL_AUDIO_STREAM_ERROR` | `LOCAL_AUDIO_STREAM_REASON` | + | `LOCAL_VIDEO_STREAM_ERROR` | `LOCAL_VIDEO_STREAM_REASON` | + | `DIRECT_CDN_STREAMING_ERROR` | `DIRECT_CDN_STREAMING_REASON` | + | `MEDIA_PLAYER_ERROR` | `MEDIA_PLAYER_REASON` | + | `RTMP_STREAM_PUBLISH_ERROR` | `RTMP_STREAM_PUBLISH_REASON` | + + **Note:** For specific renaming of enumerations, please refer to [API changes](#api-changes). + +2. **Channel media relay** + + To improve interface usability, this release removes some methods and callbacks for channel media relay. Use the alternative options listed in the table below: + + | Deleted methods and callbacks | Alternative methods and callbacks | + | ------------------------------------------------------------ | ---------------------------------- | + |
    • `startChannelMediaRelay`
    • `updateChannelMediaRelay`
    | `startOrUpdateChannelMediaRelay` | + |
    • `startChannelMediaRelayEx`
    • `updateChannelMediaRelayEx`
    | `startOrUpdateChannelMediaRelayEx` | + | `onChannelMediaRelayEvent` | `onChannelMediaRelayStateChanged` | + +#### New features + +1. **Query device score** + + This release adds the QueryDeviceScore method to query the device's score level to ensure that the user-set parameters do not exceed the device's capabilities. For example, in HD or UHD video scenarios, you can first call this method to query the device's score. If the returned score is low (for example, below 60), you need to lower the video resolution to avoid affecting the video experience. The minimum device score required for different business scenarios is varied. For specific score recommendations, please contact [technical support](mailto:support@agora.io). + +1. **Select different audio tracks for local playback and streaming** + + This release introduces the SelectMultiAudioTrack method that allows you to select different audio tracks for local playback and streaming to remote users. For example, in scenarios like online karaoke, the host can choose to play the original sound locally and publish the accompaniment in the channel. Before using this function, you need to open the media file through the OpenWithMediaSource method and enable this function by setting the `enableMultiAudioTrack` parameter in MediaSource. + +1. **Others** + + This release has passed the test verification of the following APIs and can be applied to the entire series of RTC 4.x SDK. + + - SetRemoteSubscribeFallbackOption: Sets fallback option for the subscribed video stream in weak network conditions. + - OnRemoteSubscribeFallbackToAudioOnly: Occurs when the subscribed video stream falls back to audio-only stream due to weak network conditions or switches back to the video stream after the network conditions improve. + - GetRecordingDeviceVolume(Windows): Sets the volume of the audio capturing device. + - SetPlayerOption and SetPlayerOption: Sets media player options for providing technical previews or special customization features. + - EnableCustomAudioLocalPlayback: Sets whether to enable the local playback of external audio source. + +#### Improvements + +1. **SDK task processing scheduling optimization** + + This release optimizes the scheduling mechanism for internal tasks within the SDK, with improvements in the following aspects: + + - The speed of video rendering and audio playback for both remote and local first frames improves by 10% to 20%. + - The API call duration and response time are reduced by 5% to 50%. + - The SDK's parallel processing capability significantly improves, delivering higher video quality (720P, 24 fps) even on lower-end devices. Additionally, image processing remains more stable in scenarios involving high resolutions and frame rates. + - The stability of the SDK is further enhanced, leading to a noticeable decrease in the crash rate across various specific scenarios. + +1. **In-ear monitoring volume boost** + + This release provides users with more flexible in-ear monitoring audio adjustment options, supporting the ability to set the in-ear monitoring volume to four times the original volume by calling SetInEarMonitoringVolume. + +1. **Spatial audio effects usability improvement** + + - This release optimizes the design of the SetZones method, supporting the ability to set the `zones` parameter to `NULL`, indicating the clearing of all echo cancellation zones. + - As of this release, it is no longer necessary to unsubscribe from the audio streams of all remote users within the channel before calling the methods in ILocalSpatialAudioEngine. + +1. **Other improvements** + + This release also includes the following improvements: + + - This release optimizes the SDK's domain name resolution strategy, improving the stability of calling to resolve domain names in complex network environments. + - When passing in an image with transparent background as the virtual background image, the transparent background can be filled with customized color. + - This release adds the `earMonitorDelay` and `aecEstimatedDelay` members in LocalAudioStats to report ear monitor delay and acoustic echo cancellation (AEC) delay, respectively. + - The OnPlayerCacheStats callback is added to report the statistics of the media file being cached. This callback is triggered once per second after file caching is started. + - The OnPlayerPlaybackStats callback is added to report the statistics of the media file being played. This callback is triggered once per second after the media file starts playing. You can obtain information like the audio and video bitrate of the media file through PlayerPlaybackStats. + +#### Issues fixed + +This release fixed the following issues: + +- When sharing two screen sharing video streams simultaneously, the reported `captureFrameRate` in the OnLocalVideoStats callback is 0, which is not as expected. + +#### API changes + +**Added** + +- EnableCustomAudioLocalPlayback +- QueryDeviceScore +- The `CUSTOM_VIDEO_SOURCE` enumeration in MEDIA_SOURCE_TYPE +- The `ROUTE_BLUETOOTH_DEVICE_A2DP` enumeration in AudioRoute +- SelectMultiAudioTrack +- OnPlayerCacheStats +- OnPlayerPlaybackStats +- PlayerPlaybackStats + +**Modified** + +- All `ERROR` fields in the following enumerations are changed to `REASON`: + + - `LOCAL_AUDIO_STREAM_ERROR_OK` + - `LOCAL_AUDIO_STREAM_ERROR_FAILURE` + - `LOCAL_AUDIO_STREAM_ERROR_DEVICE_NO_PERMISSION` + - `LOCAL_AUDIO_STREAM_ERROR_DEVICE_BUSY` + - `LOCAL_AUDIO_STREAM_ERROR_RECORD_FAILURE` + - `LOCAL_AUDIO_STREAM_ERROR_ENCODE_FAILURE` + - `LOCAL_AUDIO_STREAM_ERROR_RECORD_INVALID_ID` + - `LOCAL_AUDIO_STREAM_ERROR_PLAYOUT_INVALID_ID` + - `LOCAL_VIDEO_STREAM_ERROR_OK` + - `LOCAL_VIDEO_STREAM_ERROR_FAILURE` + - `LOCAL_VIDEO_STREAM_ERROR_DEVICE_NO_PERMISSION` + - `LOCAL_VIDEO_STREAM_ERROR_DEVICE_BUSY` + - `LOCAL_VIDEO_STREAM_ERROR_CAPTURE_FAILURE` + - `LOCAL_VIDEO_STREAM_ERROR_CODEC_NOT_SUPPORT` + - `LOCAL_VIDEO_STREAM_ERROR_DEVICE_NOT_FOUND` + - `LOCAL_VIDEO_STREAM_ERROR_DEVICE_DISCONNECTED` + - `LOCAL_VIDEO_STREAM_ERROR_DEVICE_INVALID_ID` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_MINIMIZED` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_CLOSED` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_OCCLUDED` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_NO_PERMISSION` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_PAUSED` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_RESUMED` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_HIDDEN` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_RECOVER_FROM_HIDDEN` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_RECOVER_FROM_MINIMIZED` + - `LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_FAILURE` + - `LOCAL_VIDEO_STREAM_ERROR_DEVICE_SYSTEM_PRESSURE` + - `DIRECT_CDN_STREAMING_ERROR_OK` + - `DIRECT_CDN_STREAMING_ERROR_FAILED` + - `DIRECT_CDN_STREAMING_ERROR_AUDIO_PUBLICATION` + - `DIRECT_CDN_STREAMING_ERROR_VIDEO_PUBLICATION` + - `DIRECT_CDN_STREAMING_ERROR_NET_CONNECT` + - `DIRECT_CDN_STREAMING_ERROR_BAD_NAME` + - `PLAYER_ERROR_NONE` + - `PLAYER_ERROR_INVALID_ARGUMENTS` + - `PLAYER_ERROR_INTERNAL` + - `PLAYER_ERROR_NO_RESOURCE` + - `PLAYER_ERROR_INVALID_MEDIA_SOURCE` + - `PLAYER_ERROR_UNKNOWN_STREAM_TYPE` + - `PLAYER_ERROR_OBJ_NOT_INITIALIZED` + - `PLAYER_ERROR_CODEC_NOT_SUPPORTED` + - `PLAYER_ERROR_VIDEO_RENDER_FAILED` + - `PLAYER_ERROR_INVALID_STATE` + - `PLAYER_ERROR_URL_NOT_FOUND` + - `PLAYER_ERROR_INVALID_CONNECTION_STATE` + - `PLAYER_ERROR_SRC_BUFFER_UNDERFLOW` + - `PLAYER_ERROR_INTERRUPTED` + - `PLAYER_ERROR_NOT_SUPPORTED` + - `PLAYER_ERROR_TOKEN_EXPIRED` + - `PLAYER_ERROR_UNKNOWN` + - `RTMP_STREAM_PUBLISH_ERROR_OK` + - `RTMP_STREAM_PUBLISH_ERROR_INVALID_ARGUMENT` + - `RTMP_STREAM_PUBLISH_ERROR_ENCRYPTED_STREAM_NOT_ALLOWED` + - `RTMP_STREAM_PUBLISH_ERROR_CONNECTION_TIMEOUT` + - `RTMP_STREAM_PUBLISH_ERROR_INTERNAL_SERVER_ERROR` + - `RTMP_STREAM_PUBLISH_ERROR_RTMP_SERVER_ERROR` + - `RTMP_STREAM_PUBLISH_ERROR_TOO_OFTEN` + - `RTMP_STREAM_PUBLISH_ERROR_REACH_LIMIT` + - `RTMP_STREAM_PUBLISH_ERROR_NOT_AUTHORIZED` + - `RTMP_STREAM_PUBLISH_ERROR_STREAM_NOT_FOUND` + - `RTMP_STREAM_PUBLISH_ERROR_FORMAT_NOT_SUPPORTED` + - `RTMP_STREAM_PUBLISH_ERROR_NOT_BROADCASTER` + - `RTMP_STREAM_PUBLISH_ERROR_TRANSCODING_NO_MIX_STREAM` + - `RTMP_STREAM_PUBLISH_ERROR_NET_DOWN` + - `RTMP_STREAM_PUBLISH_ERROR_INVALID_PRIVILEGE` + - `RTMP_STREAM_UNPUBLISH_ERROR_OK` + +**Deleted** + +- `StartChannelMediaRelay` +- `UpdateChannelMediaRelay` +- `StartChannelMediaRelayEx` +- `UpdateChannelMediaRelayEx` +- `OnChannelMediaRelayEvent` +- `CHANNEL_MEDIA_RELAY_EVENT` ### v4.2.6

    Y~k?`>2d((UIcalfx2$pefs2P)P!}&lc>(em5u6xvQ1ZN zf>BXtSo`R-q%OEZU-823OFc$?JqluEz;C^n8yvU{w@~f0G!f{j1vVW#!Ps2)=)0VY zOfG%~{U!_oZBQ3X+b>4lCi|#gnbwi~(+Sz%jNoqzF29AKVxDe_8APB=NXe4b6A@Pj zU3PVkBL)XrI_wIa@wdK0@pD7$XUPe=NvrR939AzxikuufF;__PXRpiIKMOyrl0Gco z^ddT`5(k7ulv;$_TC>=?vl_hr#OA_cMlU@1TvhD5fiK2`wT?b2GxbdxS5jChb3Hgp zrIZCA_xNs~?LV%vb5eB}ACYdSc;p?4Ow3#9f49$3(`40(S&lVFNta)T3 _v)&_> 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 O)yB&1JlVqg-ah~#AsYk$ diff --git a/assets/images/media-gateway/obs-setting-zerolatency.png b/assets/images/media-gateway/obs-setting-zerolatency.png deleted file mode 100644 index 48824594850fa1e79a68bf233e3aebf3c7d4144b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42522 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 JYO^c%{|Uxro^1dC diff --git a/assets/images/media-gateway/overview-page-image.png b/assets/images/media-gateway/overview-page-image.png deleted file mode 100644 index 4664353bbb4eabc7bceb4c0eecdf9486bacdcefb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32914 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 by youProvided by AgoraUserUserWeb serverWeb serverYour app serverYour app serverYourthird-partycloud storageYourthird-partycloud storageAgoraCloud RecordingAgoraCloud RecordingStart recordingGet recording resourcesresource IDStart recordingNotification 40: `recorder_started` <status>200 OKNotifications sent and acknowledged for all recording eventsStop recordingEnd recordingNotification 30: `uploader_started` <status>200 OKUpload recording fileNotification 31: `uploaded` <status>200 OKNotification 11: `session_exit` <exitStatus>200 OK \ No newline at end of file +Implemented by youProvided by AgoraUserUserWeb serverWeb serverYour app serverYour app serverYourthird-partycloud storageYourthird-partycloud storageAgoraCloud RecordingAgoraCloud RecordingStart recordingGet recording resourcesresource IDStart recordingNotification 40: `recorder_started` <status>200 OKNotifications sent and acknowledged for all recording eventsStop recordingEnd recordingNotification 30: `uploader_started` <status>200 OKUpload recording fileNotification 31: `uploaded` <status>200 OKNotification 11: `session_exit` <exitStatus>200 OK \ No newline at end of file diff --git a/assets/images/notification-center-service/ncs-media-pull.svg b/assets/images/notification-center-service/ncs-media-pull.svg index 420cfe301..66a6c67fb 100644 --- a/assets/images/notification-center-service/ncs-media-pull.svg +++ b/assets/images/notification-center-service/ncs-media-pull.svg @@ -1 +1 @@ -Implemented by youProvided by AgoraUserUserWeb serverWeb serverYour app serverYour app serverAgoraMedia PullAgoraMedia PullStart pulling mediainto an Agora channelCreate player APIResponse containing player parametersNotification eventType=1: Player createdAuthenticate notification signature200 OKChange player statusUpdate player APIResponse containing player parametersNotification eventType=4: Player status changedAuthenticate notification signature200 OKStop playing mediaDelete player APIResponse confirming player deleteNotification eventType=2: Player destroyedAuthenticate notification signature200 OK \ No newline at end of file +Implemented by youProvided by AgoraUserUserWeb serverWeb serverYour app serverYour app serverAgoraMedia PullAgoraMedia PullStart pulling mediainto an Agora channelCreate player APIResponse containing player parametersNotification eventType=1: Player createdAuthenticate notification signature200 OKChange player statusUpdate player APIResponse containing player parametersNotification eventType=4: Player status changedAuthenticate notification signature200 OKStop playing mediaDelete player APIResponse confirming player deleteNotification eventType=2: Player destroyedAuthenticate notification signature200 OK \ No newline at end of file diff --git a/assets/images/notification-center-service/ncs-media-push.svg b/assets/images/notification-center-service/ncs-media-push.svg index aab6eab79..2ea74fd43 100644 --- a/assets/images/notification-center-service/ncs-media-push.svg +++ b/assets/images/notification-center-service/ncs-media-push.svg @@ -1 +1 @@ -Implemented by youProvided by AgoraUserUserWeb serverWeb serverYour app serverYour app serverAgoraMedia PushAgoraMedia PushStart media pushCreate a converterResponse confirming conveter creationNotification eventType=1: Converter createdAuthenticate notification signature200 OKChange converter configurationUpdate converter APIResponse confirming configuration updateNotification eventType=2: Converter configuration changedAuthenticate notification signature200 OKNotification eventType=3: Converter status changedAuthenticate notification signature200 OKStop media pushDelete converter APIResponse confirming converter deleteNotification eventType=4: Converter destroyedAuthenticate notification signature200 OK \ No newline at end of file +Implemented by youProvided by AgoraUserUserWeb serverWeb serverYour app serverYour app serverAgoraMedia PushAgoraMedia PushStart media pushCreate a converterResponse confirming conveter creationNotification eventType=1: Converter createdAuthenticate notification signature200 OKChange converter configurationUpdate converter APIResponse confirming configuration updateNotification eventType=2: Converter configuration changedAuthenticate notification signature200 OKNotification eventType=3: Converter status changedAuthenticate notification signature200 OKStop media pushDelete converter APIResponse confirming converter deleteNotification eventType=4: Converter destroyedAuthenticate notification signature200 OK \ No newline at end of file diff --git a/assets/images/others/authentication-logic.svg b/assets/images/others/authentication-logic.svg index 7e964a030..71c783045 100644 --- a/assets/images/others/authentication-logic.svg +++ b/assets/images/others/authentication-logic.svg @@ -1,450 +1 @@ -Implemented by youProvided by AgoraToken serverToken serverAppAppSD-RTNSD-RTNJoin a channel with authenticationRequest a token using channel name,role, token type, and user IDValidate user againstinternal securityGenerate a token and return it to the clientJoin a channel with uid,channel name, and tokenValidatethe tokenTrigger the callback afteradding user to the channelRenew tokenTrigger event: Token Privilege will ExpireRequest a fresh token using channel name,role, token type, and user IDValidate user request against internal logicGenerate a fresh token and return it to the clientSend the fresh tokenwith a RenewToken request \ No newline at end of file +Implemented by youProvided by AgoraToken serverToken serverAppAppSD-RTNSD-RTNJoin a channel with authenticationRequest a token using channel name,role, token type, and user IDValidate user againstinternal securityGenerate a token and return it to the clientJoin a channel with uid,channel name, and tokenValidatethe tokenTrigger the callback afteradding user to the channelRenew tokenTrigger event: Token Privilege will ExpireRequest a fresh token using channel name,role, token type, and user IDValidate user request against internal logicGenerate a fresh token and return it to the clientSend the fresh tokenwith a RenewToken request \ No newline at end of file diff --git a/assets/images/others/documentation_way_of_working.svg b/assets/images/others/documentation_way_of_working.svg index 32777d4db..f80109bbe 100644 --- a/assets/images/others/documentation_way_of_working.svg +++ b/assets/images/others/documentation_way_of_working.svg @@ -1,510 +1 @@ -Product OwnerProduct OwnerProject ManagerProject ManagerDeveloperDeveloperTesterTesterTechnical WriterTechnical WriterDevRellerDevRellerRequest implementation of new featuresInform DevRel about new featuresFeature implementationCreate JIRA issues for each feature requirementThis includes documentation issues andusage details for the featureloop[For each JIRA issue]Implementation and documentation tasks completed in parallelRequest implementationRequest documentationImplement featureWrite documentation from requirement,implementation and Developer inputloop[Validate]Request implementation validationValidate implementation against requirementIf necessary, request changesUpdateImplementation approvedloop[Validate]Request documentation validationRequest documentation validationValidate documentation against HLDTechnical validationIf necessary, request changesIf necessary, request changesUpdatePeer review if feature requiresa large documentation changeUpdateDocumentation approvedHLD implementation complete \ No newline at end of file +Welcome to PlantUML! You can start with a simple UML Diagram like: Bob->Alice: Hello Or class Example You will find more information about PlantUML syntax onhttps://plantuml.com (Details by typinglicensekeyword) PlantUML 1.2023.12[From documentation_way_of_working.puml (line 4) ] @startuml !include ../images/agora_skin.iumlcannot include ../images/agora_skin.iuml \ No newline at end of file diff --git a/assets/images/others/media-stream-encryption.svg b/assets/images/others/media-stream-encryption.svg index 692c72a8d..be214faa5 100644 --- a/assets/images/others/media-stream-encryption.svg +++ b/assets/images/others/media-stream-encryption.svg @@ -1,462 +1 @@ -Implemented by youProvided by AgoraUserUserAppAppDeveloper'sAuthentication SystemDeveloper'sAuthentication SystemAPIAPIStart the appInitiate the Video SDK engineMedia stream encryptionLogin to the developer'sauthentication system.Request a 32-byte keyGenerate a 32-bytestring using OpenSSLRequested keyRequest a 32-byte salt inBase64 formatGenerate a 32-bytestring in Base64format using OpenSSLRequested saltCreate a encryption configuration usingthe received salt and keyCall the method to set theencryption configurationSelect a channel to joinJoin a channel with user Id, channel name, and tokenJoin acceptedEncrypted media stream \ No newline at end of file +Implemented by youProvided by AgoraUserUserAppAppDeveloper'sAuthentication SystemDeveloper'sAuthentication SystemAPIAPIStart the appInitiate the Video SDK engineMedia stream encryptionLogin to the developer'sauthentication system.Request a 32-byte keyGenerate a 32-bytestring using OpenSSLRequested keyRequest a 32-byte salt inBase64 formatGenerate a 32-bytestring in Base64format using OpenSSLRequested saltCreate a encryption configuration usingthe received salt and keyCall the method to set theencryption configurationSelect a channel to joinJoin a channel with user Id, channel name, and tokenJoin acceptedEncrypted media stream \ No newline at end of file diff --git a/assets/images/others/play-media.svg b/assets/images/others/play-media.svg index edbf6e3e4..576767e5c 100644 --- a/assets/images/others/play-media.svg +++ b/assets/images/others/play-media.svg @@ -1,468 +1 @@ -Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppUse Video SDK to create an instance of Agora EngineEnable audio and video in the engineJoinJoin a channelRetrieve authentication token to join a channelJoin the channelPlay media filesSelect media fileUse Video SDK to create an instance of Media PlayerOpen media file using Media PlayerOpen media file completedPlay media fileSet up local video panelto display Media Player outputUpdate channel media optionsto publish Media Player outputPlay the media file on the Media PlayerPause or resume playCall pause or resume methodsMedia file played completelyResume publishing of camera and microphone \ No newline at end of file +Your appAgoraUserUserVideo SDKVideo SDKSD-RTNSD-RTNOpen AppUse Video SDK to create an instance of Agora EngineEnable audio and video in the engineJoinJoin a channelRetrieve authentication token to join a channelJoin the channelPlay media filesSelect media fileUse Video SDK to create an instance of Media PlayerOpen media file using Media PlayerOpen media file completedPlay media fileSet up local video panelto display Media Player outputUpdate channel media optionsto publish Media Player outputPlay the media file on the Media PlayerPause or resume playCall pause or resume methodsMedia file played completelyResume publishing of camera and microphone \ No newline at end of file diff --git a/assets/images/real-time-transcription/real-time-transcription.png b/assets/images/real-time-transcription/real-time-transcription.png deleted file mode 100644 index a16249aa0802be58f8f59ede83000a158c2420e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3828 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 - - - - - - diff --git a/assets/images/shared/ncs-worflow.svg b/assets/images/shared/ncs-worflow.svg new file mode 100644 index 000000000..6dff5e073 --- /dev/null +++ b/assets/images/shared/ncs-worflow.svg @@ -0,0 +1 @@ +Implemented by youProvided by AgoraUserUserWeb serverWeb serverAppAppSD-RTNSD-RTNLoginLogin authenticationNo notificationCreate channelCreate channelOpen channelNotification 101:`channel name` opened at `time`.Authenticate notificationsignature200 OKNotifications sent and acknowledged for all channel eventsClose channelNotification 102:`channel name` destroyed at `time`.Authenticate notificationsignature200 OKLogoutLogoutNo notification \ No newline at end of file diff --git a/assets/images/signaling/authentication-workflow.puml b/assets/images/signaling/authentication-workflow.puml index d75935825..890eb03fd 100644 --- a/assets/images/signaling/authentication-workflow.puml +++ b/assets/images/signaling/authentication-workflow.puml @@ -44,5 +44,4 @@ API -> API: Validate the token API -> APP: Trigger callback end -@enduml - +@enduml \ No newline at end of file diff --git a/assets/images/signaling/authentication-workflow.svg b/assets/images/signaling/authentication-workflow.svg index b800f1560..efa0ea6f0 100644 --- a/assets/images/signaling/authentication-workflow.svg +++ b/assets/images/signaling/authentication-workflow.svg @@ -1 +1 @@ -Implemented by youAgoraToken serverToken serverYour appYour appSignalingSignalingLog in to Signaling using authenticationConfigure your token server usingyour App Id and App CertificateRequest an RTM token using a user IdValidate request against internal securityand generate a tokenReturn RTM token to AppInitiate the Signaling Engine usingthe App IdLog in to Signaling usingthe user Id and the RTM tokenValidate the tokenLog in user, then trigger callbackJoin a stream channelRequest an RTC token usinga user Id, and a stream channel nameValidate request against internal securityand generate tokenReturn RTC token to AppJoin a stream channel usingchannel name, user Id, and the RTC tokenRenew tokenTrigger event:token privilege will expireRequest an RTM token usingthe user IDValidate request against internal securityand generate a tokenReturn RTM token to AppSend RTM token to Signaling with acall to renew tokenValidate the tokenTrigger callback \ No newline at end of file +Implemented by youAgoraToken serverToken serverYour appYour appSignalingSignalingLog in to Signaling using authenticationConfigure your token server usingyour App Id and App CertificateRequest an RTM token using a user IdValidate request against internal securityand generate a tokenReturn RTM token to AppInitiate the Signaling Engine usingthe App IdLog in to Signaling usingthe user Id and the RTM tokenValidate the tokenLog in user, then trigger callbackJoin a stream channelRequest an RTC token usinga user Id, and a stream channel nameValidate request against internal securityand generate tokenReturn RTC token to AppJoin a stream channel usingchannel name, user Id, and the RTC tokenRenew tokenTrigger event:token privilege will expireRequest an RTM token usingthe user IDValidate request against internal securityand generate a tokenReturn RTM token to AppSend RTM token to Signaling with acall to renew tokenValidate the tokenTrigger callback \ No newline at end of file diff --git a/assets/images/signaling/cloud-proxy.svg b/assets/images/signaling/cloud-proxy.svg index 45837fb4e..df6ffca59 100644 --- a/assets/images/signaling/cloud-proxy.svg +++ b/assets/images/signaling/cloud-proxy.svg @@ -1 +1 @@ -Implemented by youProvided by AgoraUserUserAdminAdminAppAppEnterprise FirewallEnterprise FirewallCloud ProxyCloud ProxySD-RTNSD-RTNWhitelist IP addresses and portsfor Cloud Proxy in the firewall.Open the appInitialize the Signaling EngineEnable cloud proxyCall the method to enablea Cloud Proxy connectionRequest access toCloud ProxyCheck whitelist to grantaccessRequest access toCloud ProxyProxy informationJoin a channelJoin a channelAsk to join a channelJoin successJoin successSend data streamSend and receive data streamReceive data streamLeave a channelLeave the channelChannel leave \ No newline at end of file +Implemented by youProvided by AgoraUserUserAdminAdminAppAppEnterprise FirewallEnterprise FirewallCloud ProxyCloud ProxySD-RTNSD-RTNWhitelist IP addresses and portsfor Cloud Proxy in the firewall.Open the appInitialize the Signaling EngineEnable cloud proxyCall the method to enablea Cloud Proxy connectionRequest access toCloud ProxyCheck whitelist to grantaccessRequest access toCloud ProxyProxy informationJoin a channelJoin a channelAsk to join a channelJoin successJoin successSend data streamSend and receive data streamReceive data streamLeave a channelLeave the channelChannel leave \ No newline at end of file diff --git a/assets/images/signaling/configure_project.png b/assets/images/signaling/configure_project.png new file mode 100644 index 0000000000000000000000000000000000000000..2c55e2d3de76338818c80243b4aa3a61e6cfbacc GIT binary patch literal 47633 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�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 B4A%ev literal 0 HcmV?d00001 diff --git a/assets/images/signaling/data-encryption.puml b/assets/images/signaling/data-encryption.puml index 8c3de89d2..e590cbe34 100644 --- a/assets/images/signaling/data-encryption.puml +++ b/assets/images/signaling/data-encryption.puml @@ -30,4 +30,4 @@ APP <-> API: Join a channel APP <-> API: Communicate over an\n encrypted data stream -@enduml +@enduml \ No newline at end of file diff --git a/assets/images/signaling/data-encryption.svg b/assets/images/signaling/data-encryption.svg index 150c253d6..5d2deb717 100644 --- a/assets/images/signaling/data-encryption.svg +++ b/assets/images/signaling/data-encryption.svg @@ -1 +1 @@ -Implemented by youProvided by AgoraUserUserAuthentication systemAuthentication systemAppAppSD-RTNSD-RTNStart the appSetup media stream encryptionLogin to theauthentication systemRetrieve a 32-byte keyRetrieve a 32-byte salt inBase64 formatCreate a encryption configuration usingthe key and saltSet the encryption configurationInitiate the Signaling EngineSelect a channel to joinRetrieve an access token.Join a channelCommunicate over anencrypted data stream \ No newline at end of file +Implemented by youProvided by AgoraUserUserAuthentication systemAuthentication systemAppAppSD-RTNSD-RTNStart the appSetup media stream encryptionLogin to theauthentication systemRetrieve a 32-byte keyRetrieve a 32-byte salt inBase64 formatCreate a encryption configuration usingthe key and saltSet the encryption configurationInitiate the Signaling EngineSelect a channel to joinRetrieve an access token.Join a channelCommunicate over anencrypted data stream \ No newline at end of file diff --git a/assets/images/signaling/get-started-workflow.puml b/assets/images/signaling/get-started-workflow.puml index dc9a4d0e3..7fa3d1154 100644 --- a/assets/images/signaling/get-started-workflow.puml +++ b/assets/images/signaling/get-started-workflow.puml @@ -35,5 +35,4 @@ USR -> APP: Log out APP -> API: Log out of Signaling end -@enduml - +@enduml \ No newline at end of file diff --git a/assets/images/signaling/get-started-workflow.svg b/assets/images/signaling/get-started-workflow.svg index 8172ffe92..67c35e610 100644 --- a/assets/images/signaling/get-started-workflow.svg +++ b/assets/images/signaling/get-started-workflow.svg @@ -1 +1 @@ -Your appAgoraUserUserSignaling SDKSignaling SDKSignalingSignalingInitializeOpen appCreate a signalingEngine instanceAdd event listenersSet the authentication tokenLog in to SignalingMessagesSubscribe to a channelsignalingEngine.subscribePublish messagesignalingEngine.publishListen for message eventsmessageReceive messageCloseLog outLog out of Signaling \ No newline at end of file +Your appAgoraUserUserSignaling SDKSignaling SDKSignalingSignalingInitializeOpen appCreate a signalingEngine instanceAdd event listenersSet the authentication tokenLog in to SignalingMessagesSubscribe to a channelsignalingEngine.subscribePublish messagesignalingEngine.publishListen for message eventsmessageReceive messageCloseLog outLog out of Signaling \ No newline at end of file diff --git a/assets/images/signaling/presence-workflow.puml b/assets/images/signaling/presence-workflow.puml index f946bfcda..826225bc9 100644 --- a/assets/images/signaling/presence-workflow.puml +++ b/assets/images/signaling/presence-workflow.puml @@ -47,5 +47,4 @@ USR -> APP: Log out APP -> API: Log out of Signaling end -@enduml - +@enduml \ No newline at end of file diff --git a/assets/images/signaling/presence-workflow.svg b/assets/images/signaling/presence-workflow.svg index d3b2bf982..18d5029f2 100644 --- a/assets/images/signaling/presence-workflow.svg +++ b/assets/images/signaling/presence-workflow.svg @@ -1 +1 @@ -Your appAgoraUserUserSignaling SDKSignaling SDKSignalingSignalingInitializeOpen appCreate a signalingEngine instanceAdd the presence event listenerSet the authentication tokenLog in to SignalingPresence notificationsJoin a channelSubscribe to a channeland enable presence events for the channelPresence event:A remote user joins, leavesor changes status in the channelRead the event argumentsInform usersPresence queriesLoad the list of users in the channelCall getOnlineUsersLoad the channel list for a userCall getUserChannelsUser statusSet own statusCall setStateQuery the status of a remote userCall getStateCloseLog outLog out of Signaling \ No newline at end of file +Your appAgoraUserUserSignaling SDKSignaling SDKSignalingSignalingInitializeOpen appCreate a signalingEngine instanceAdd the presence event listenerSet the authentication tokenLog in to SignalingPresence notificationsJoin a channelSubscribe to a channeland enable presence events for the channelPresence event:A remote user joins, leavesor changes status in the channelRead the event argumentsInform usersPresence queriesLoad the list of users in the channelCall getOnlineUsersLoad the channel list for a userCall getUserChannelsUser statusSet own statusCall setStateQuery the status of a remote userCall getStateCloseLog outLog out of Signaling \ No newline at end of file diff --git a/assets/images/signaling/project_settings_signaling.png b/assets/images/signaling/project_settings_signaling.png new file mode 100644 index 0000000000000000000000000000000000000000..91f3d902299add0540d0e6e73e8a8e3ff9961754 GIT binary patch literal 108708 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 z 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 API: Unsubscribe from user metadata update notifications APP -> API: Log out of Signaling end -@enduml - +@enduml \ No newline at end of file diff --git a/assets/images/signaling/signaling-metadata-workflow.svg b/assets/images/signaling/signaling-metadata-workflow.svg index 0b66cffcf..ff5f3c62d 100644 --- a/assets/images/signaling/signaling-metadata-workflow.svg +++ b/assets/images/signaling/signaling-metadata-workflow.svg @@ -1 +1 @@ -Your appAgoraUserUserSignaling SDKSignaling SDKSignalingSignalingInitializeOpen appCreate a Signaling clientAdd client event callbacksCreate a Signaling channelAdd channel event callbacksLog in to SignalingJoin a channelRead and write user metadataSet local user metadataChange user informationAdd, update, or delete local user metadataSubscribe to metadata of remote usersOn user metadata updated callbackRetrieve metadata of a remote userRead and write channel metadataSet channel metadataUpdate channel informationAdd, update, or delete channel metadataOn channel metadata updated callbackRetrieve channel metadataUse locksSet a lockUpdate channel metadata with lockingAcquire the lockSpecify the lock name when updating metadataCloseLeave the channelRelease locksUnsubscribe from user metadata update notificationsLog out of Signaling \ No newline at end of file +Your appAgoraUserUserSignaling SDKSignaling SDKSignalingSignalingInitializeOpen appCreate a Signaling clientAdd client event callbacksCreate a Signaling channelAdd channel event callbacksLog in to SignalingJoin a channelRead and write user metadataSet local user metadataChange user informationAdd, update, or delete local user metadataSubscribe to metadata of remote usersOn user metadata updated callbackRetrieve metadata of a remote userRead and write channel metadataSet channel metadataUpdate channel informationAdd, update, or delete channel metadataOn channel metadata updated callbackRetrieve channel metadataUse locksSet a lockUpdate channel metadata with lockingAcquire the lockSpecify the lock name when updating metadataCloseLeave the channelRelease locksUnsubscribe from user metadata update notificationsLog out of Signaling \ No newline at end of file diff --git a/assets/images/signaling/signaling-pricing-plans.png b/assets/images/signaling/signaling-pricing-plans.png new file mode 100644 index 0000000000000000000000000000000000000000..fc398b5f9c601184cd4efe694485237f22339609 GIT binary patch literal 55371 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