RTC Web SDK for browsers, H5, and WebView
Browser Web, mobile H5, Android WebView, and iOS WKWebView share @akstream/rtc-web-sdk for meeting logic. A WebView host still owns certificate trust, camera/microphone permission prompts, foreground/background behavior, and native screen sharing where available. Use HTTPS trusted by the real device; a localhost development exception does not extend to a phone's LAN address.
flowchart LR
A["Backend provides join ticket"] --> B["SDK join and admission"]
B --> C["Remote onRemoteTrack"]
C --> D["Page calls attachRemoteMedia"]
D --> E["SDK leave and view cleanup"]
Join receive-only first
Install the Web package from the same SDK delivery. Your backend returns serverUrl, roomId, and one-use accessTicket after business authorization. Do not place the ticket in a shareable page URL.
If your delivery contains an npm tarball, run npm install ./vendor/akstream-rtc-web-sdk.tgz from the application root. For a source package, retain its src, dist, and offline whiteboard resources rather than copying only the entry JS. A same-name public npm version is not proof of a matching delivery.
<div id="remote"></div>
<p id="meeting-status" role="status"></p>
The following runs in an ESM-bundled page. meeting is the join response from your backend:
import { AkRtcWebClient } from '@akstream/rtc-web-sdk'
const remote = document.getElementById('remote')
const status = document.getElementById('meeting-status')
const views = new Map()
const client = new AkRtcWebClient(meeting.serverUrl, {
onState: (state, detail) => { status.textContent = detail || state },
onRemoteTrack: ({ subscriptionId, track }) => {
const element = document.createElement(track.kind === 'audio' ? 'audio' : 'video')
element.autoplay = true
if (element instanceof HTMLVideoElement) element.playsInline = true
remote.append(element)
views.set(subscriptionId, element)
void client.attachRemoteMedia(subscriptionId, element)
},
onRemoteTrackRemoved: id => {
const element = views.get(id)
if (element) { client.detachRemoteMedia(id, element); element.remove() }
views.delete(id)
}
})
const joined = await client.join(meeting.roomId, meeting.accessTicket, { platform: 'pc-web' })
if (joined.admissionStatus === 'Waiting') status.textContent = 'Waiting for host admission'
async function leaveMeeting() {
await client.leave()
for (const element of views.values()) element.remove()
views.clear()
}
After admission, success means the client can see members and receive remote publications. Receive-only needs no local camera or microphone permission. Once this works, enable capture from a user gesture with startCameraAndMicrophone({audio:true,video:true}) and pass its stream to attachLocalPreview. Keep receive-only available if OS permission is denied.
Move from receive-only to real publishing
Put an Enable camera and microphone button behind a user gesture. Check current SDK capabilities and host restrictions, call startCameraAndMicrophone({audio:true,video:true}), then pass the returned MediaStream to attachLocalPreview(localVideo, stream). Mute the local preview element to prevent feedback. Ask a second device to confirm actual remote frames and sound; a local preview alone does not prove WHIP publishing worked.
Disable camera and microphone separately with setTrackEnabled('CameraVideo', false) and setTrackEnabled('MicrophoneAudio', false). Drive button labels from onLocalMediaState, not by flipping a local boolean after a click. Browsers may block remote audio autoplay: if video appears but sound is absent, first let the user activate playback with a page click, then inspect receive stats, output route, and volume.
Call startScreenShare() only from a user click and let the browser picker choose a screen or window. Camera, microphone, and screen are separate publications; stopScreenShare() stops only sharing. Two video publications from one member need separate views and publication IDs.
What the host page owns
- Use
publicationIdto distinguish camera and screen, andsubscriptionIdto manage the view binding. A matching display name does not mean two streams are one card. - Update controls from SDK media-state callbacks. Screen sharing requires a user gesture and system picker.
- Your app owns layout and appearance. The SDK provides media, status, recording data, and resource APIs.
getContinuousRecording()andonContinuousRecordingChangedneed a matching newer server and SDK build. - Route changes and teardown must release the same client. Creating duplicates leaves multiple sessions.
An Android WebView host must handle camera/microphone permission requests and approve only resources the user actually allowed. An iOS WKWebView host likewise owns media-capture decisions. Native hosts also manage foreground/background state, audio routing, and certificate trust. The Web SDK cannot bypass OS permissions or make a LAN HTTP page a secure production capture origin.
Web Demo shows one host implementation; it is not a production business identity system. Meeting recording and attachments require independent archive access, not a reused join ticket.
For every public AkRtcWebClient method, input/result and major callback, see Web/H5 methods. Host payloads are in room actions; post-meeting calls are in archive methods.
Where to look when it fails
| Symptom | First check | Page response |
|---|---|---|
Join returns Waiting |
Waiting-room policy and host admission | Show waiting; do not start capture |
| 401 or ticket consumed | Whether the one-use ticket was replayed | Ask the backend for a fresh ticket |
| Local preview but remote has no video | Publish state, WHIP/ICE, media node, codec | Keep local preview and show the publish error |
| Video arrives without sound | Browser autoplay, audio packets, output route | Offer a click to activate audio |
| Only physical WebView fails | Host certificate, capture permissions, native routing | Record OS and SDK errors separately |