Peer-to-Peer Video Call
Prerequisites: NetCrafter Live installed; com.unity.webrtc installed (Package Manager) — version 3.0.0 or later if either side of the call is Android, see the note under Part 4 below; your own self-hosted coturn + rendezvous server up and running (see the Server Deployment Guide or just run the Server Setup Wizard); a second machine, build, or Editor instance to actually place a call to.
What you'll build: Nothing — this tutorial runs the demo scene that already ships with the package (Examples/OneToOneCallDemo/) and explains what it's doing, so you can see a real NetCrafterLiveConnection integration working before you build your own UI around it.
Open the demo scene#
Assets/PivecLabs/NetCrafterLive/Examples/OneToOneCallDemo/OneToOneCallDemo.unity
Double-click it in the Project window like any other scene. Select the Read Me First GameObject in the Hierarchy to see the same summary in the Inspector.
The scene has one NetCrafterLiveConnection GameObject and one OneToOneCallDemoController GameObject that wires a plain UI to it — that controller script is the actual reference implementation this tutorial walks through. Open OneToOneCallDemoController.cs alongside this tutorial if you want to read the wiring while you go.
Everything except the two video previews themselves — title, buttons, toggles, status/code/error labels — lives in a collapsible settings panel, visible by default. The Hide Settings/Show Settings button in the top corner (a sibling of the panel, so it's never hidden along with it) toggles it; the video previews stay on screen either way. The two previews normally sit on a screen-space Canvas as RawImages — see Part 6 below for switching them to world-space instead.
Get a server running#
If you haven't already: open Tools → Pivec Labs → NetCrafter Live, run the Server Setup Wizard tab, and get through its Finish step. That writes your Relay Host, Relay Port, Rendezvous URL, TURN Username, and Shared Secret directly into the project's Relay Settings — nothing to copy-paste by hand.
Already have a server? Confirm the Relay Settings tab shows a real Relay Host and Rendezvous URL, not blank fields.
Assign Relay Settings on the demo's component#
Select the NetCrafterLiveConnection GameObject in the Hierarchy. In its Inspector, click Assign Project's Relay Settings. This is the one manual step every fresh scene needs — the demo scene ships with connectionSettings intentionally unassigned, the same as any GameObject you'll add this component to yourself.
If you skip this step and press Host/Join anyway, you'll see a clear error in the on-screen error label and the Console (connectionSettings is not assigned...) rather than a silent failure.
Host a call#
Press Play. Click Host. Within a couple of seconds:
- The Join code label fills in with a short code — this came from
JoinCodeReady. - Status moves from
New→Connectingwhile it waits for someone to join. - Your own camera feed appears in the local preview panel, driven by
LocalVideoTextureAvailable— this is what the other peer is about to receive.
Share that join code however you like (voice, chat, screen) with whoever's joining.
Android as the sending side requires com.unity.webrtc 3.0.0 or later. Earlier pre-release versions of the package (confirmed on 3.0.0-pre.8) have a hardware video encoder bug on Android that prevents sending video — the call negotiates and connects normally, but no video ever reaches the other side from the Android device. Receiving video on Android, and Android hosting/joining a call where a PC is the one sending video, are both unaffected. If you're on an older package version and see this, update to 3.0.0 or later via Package Manager first before troubleshooting anything else.
Join from the other side#
On a second machine, a second build, or a second Unity Editor instance (File → Open Project in New Window works for local testing — remember to enable Run In Background in Player Settings on both instances), open the same demo scene, assign Relay Settings there too, paste the join code into the input field, and click Join.
Once negotiation completes on both sides:
- Status reaches
Connected(green) on both ends. - Each side's remote preview panel fills in with the other peer's video, driven by
RemoteVideoTextureAvailable. - Audio plays automatically through a hidden
AudioSource— no wiring needed unless you want to route it somewhere specific (see the API Reference'sremoteAudioOutputfield).
Click Leave on either side to hang up cleanly — this calls connection.Leave(), which tears down the peer connection and clears both preview textures.
Try the other options#
Three toggles sit in the settings panel, next to the Host/Join buttons:
- Use RenderTexture source — switches
videoSourcefrom the defaultWebcamtoRenderTexture. The demo scene already has a small rotating-cube camera rig wired up for this (DemoRenderTextureSource/DemoRotatingCube) so you can test this path without a real webcam at all. In your own scene, you'd pointcustomRenderTextureat your own game camera's output instead. - Force-Relay — sets
forceRelay = truebefore the next Host/Join, restricting the connection to TURN-relayed candidates only (no direct/STUN). Worth understanding why this exists and when you'd actually want it — see the Player Hosting Guide rather than duplicating that explanation here. Short version: it proves your relay path actually works end to end, and it's the setting most likely to expose a stale TURN credential if one's gone stale (also covered there). - World-space preview — flips
connection.videoDisplayModebetweenScreenSpace2D(default) andWorldSpace3D. With it on, the local/remote previews move from the screen-spaceRawImages to two draggable 3D panels in the scene instead — click and drag one around with the mouse; it tilts slightly as you drag it near its edges. The toggle itself is just a thin wrapper aroundconnection.SetVideoDisplayMode()— see the Player Hosting Guide's display-modes section for the full pattern (including how to react toVideoDisplayModeChangedfrom your own script) and the API Reference for the enum itself.
How it works#
OneToOneCallDemoController does nothing NetCrafterLiveConnection doesn't already expose publicly — it's a thin UI layer over:
hostButton.onClick.AddListener(() => connection.Host());
joinButton.onClick.AddListener(() => connection.Join(joinCodeInput.text));
connection.ConnectionStateChanged += state => /* update status label */;
connection.JoinCodeReady += code => /* show the code */;
connection.LocalVideoTextureAvailable += tex => localPreview.texture = tex;
connection.RemoteVideoTextureAvailable += tex => remotePreview.texture = tex;
connection.VideoDisplayModeChanged += mode => /* swap screen-space <-> world-space preview objects */;
connection.ErrorOccurred += message => /* show the error */;
That's genuinely the whole integration. Everything else in the controller script is just Unity UI bookkeeping (button listeners, toggle wiring, clearing labels on Leave).
Where to go from here#
- API Reference — the complete
NetCrafterLiveConnectionfield/event/method list. - Player Hosting Guide — what actually happens on the wire when you call
Host()/Join(), a from-scratch join-panel example to build your own UI on top of rather than copying the demo scene's, and when to use screen-space vs. world-space display. - Manual — the Relay Settings and Server Setup Wizard tabs in full.