Use a camera, OBS Virtual Camera, or microphone as an input, then choose the device names and camera output modes visible to your page. The SDK configures the browser Context; your existing client runs the page and its ordinary navigator.mediaDevices calls.
First follow SDK setup for your language. This guide assumes the SDK and your chosen automation client are installed. See runtime management to select an executable or release, and Contexts and ownership for session cleanup.
These examples require Mimic 0.2.4 or later; the SDK downloads a compatible runtime automatically. Media inputs must be available to the machine running Mimic. A device connected only to a remote automation client is not a local input to that runtime.
Capture with your client
Each example selects the default camera and microphone, falling back to the first available source of each kind. It exposes them as Studio Camera and Studio Microphone, grants capture to https://example.com, prints the resulting track settings, and stops every track before closing the session.
To use OBS, start its virtual camera and select its video source by the label returned during discovery. Keep the microphone selection independent: a virtual camera does not supply an audio input automatically.
Requires available camera and microphone sources. The source supplies capture; labels and groups define the devices the site sees.
Create files
import { launch } from "mimic-browser/playwright";
const session = await launch();
try {
const context = await session.newContext({
media: async ({ browserContextId, mimic }) => {
const { sources } = await mimic.getMediaSources({ browserContextId });
const choose = (kind) => sources.find(s => s.kind === kind && s.default)
?? sources.find(s => s.kind === kind);
const camera = choose("videoinput");
const microphone = choose("audioinput");
if (!camera || !microphone) throw new Error("Capture source unavailable");
return {
seed: "meeting-devices",
devices: [
{
key: "camera", kind: "videoinput",
source: { sourceId: camera.sourceId },
label: "Studio Camera", group: "desk",
modes: [{ width: 640, height: 480, frameRate: 30 }],
defaultMode: { width: 640, height: 480, frameRate: 30 },
processing: { resize: "crop-and-scale" },
},
{
key: "microphone", kind: "audioinput",
source: { sourceId: microphone.sourceId },
label: "Studio Microphone", group: "desk",
},
],
};
},
});
await context.grantPermissions(["camera", "microphone"], { origin: "https://example.com" });
const page = await context.newPage();
await page.goto("https://example.com");
console.log(await page.evaluate(async () =>
(await navigator.mediaDevices.enumerateDevices()).map(device => device.toJSON())
));
} finally {
await session.close();
}Run
node media.mjsRequires available camera and microphone sources. The source supplies capture; labels and groups define the devices the site sees.
Create files
import { launch } from "mimic-browser/puppeteer";
const session = await launch();
try {
const context = await session.newContext({
media: async ({ browserContextId, mimic }) => {
const { sources } = await mimic.getMediaSources({ browserContextId });
const choose = (kind) => sources.find(s => s.kind === kind && s.default)
?? sources.find(s => s.kind === kind);
const camera = choose("videoinput");
const microphone = choose("audioinput");
if (!camera || !microphone) throw new Error("Capture source unavailable");
return {
seed: "meeting-devices",
devices: [
{
key: "camera", kind: "videoinput",
source: { sourceId: camera.sourceId },
label: "Studio Camera", group: "desk",
modes: [{ width: 640, height: 480, frameRate: 30 }],
defaultMode: { width: 640, height: 480, frameRate: 30 },
processing: { resize: "crop-and-scale" },
},
{
key: "microphone", kind: "audioinput",
source: { sourceId: microphone.sourceId },
label: "Studio Microphone", group: "desk",
},
],
};
},
});
await context.setPermission("https://example.com",
{ permission: { name: "camera" }, state: "granted" },
{ permission: { name: "microphone" }, state: "granted" },
);
const page = await context.newPage();
await page.goto("https://example.com");
console.log(await page.evaluate(async () =>
(await navigator.mediaDevices.enumerateDevices()).map(device => device.toJSON())
));
} finally {
await session.close();
}Run
node media.mjsRequires available camera and microphone sources. The source supplies capture; labels and groups define the devices the site sees.
Create files
async function main() {
const { launch } = await import("mimic-browser/playwright");
const session = await launch();
try {
const context = await session.newContext({
media: async ({ browserContextId, mimic }) => {
const { sources } = await mimic.getMediaSources({ browserContextId });
const choose = (kind: "videoinput" | "audioinput") => sources.find(s => s.kind === kind && s.default)
?? sources.find(s => s.kind === kind);
const camera = choose("videoinput");
const microphone = choose("audioinput");
if (!camera || !microphone) throw new Error("Capture source unavailable");
return {
seed: "meeting-devices",
devices: [
{
key: "camera", kind: "videoinput",
source: { sourceId: camera.sourceId },
label: "Studio Camera", group: "desk",
modes: [{ width: 640, height: 480, frameRate: 30 }],
defaultMode: { width: 640, height: 480, frameRate: 30 },
processing: { resize: "crop-and-scale" },
},
{
key: "microphone", kind: "audioinput",
source: { sourceId: microphone.sourceId },
label: "Studio Microphone", group: "desk",
},
],
};
},
});
await context.grantPermissions(["camera", "microphone"], { origin: "https://example.com" });
const page = await context.newPage();
await page.goto("https://example.com");
console.log(await page.evaluate(async () =>
(await navigator.mediaDevices.enumerateDevices()).map(device => device.toJSON())
));
} finally {
await session.close();
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});Run
npx tsx media.tsRequires available camera and microphone sources. The source supplies capture; labels and groups define the devices the site sees.
Create files
async function main() {
const { launch } = await import("mimic-browser/puppeteer");
const session = await launch();
try {
const context = await session.newContext({
media: async ({ browserContextId, mimic }) => {
const { sources } = await mimic.getMediaSources({ browserContextId });
const choose = (kind: "videoinput" | "audioinput") => sources.find(s => s.kind === kind && s.default)
?? sources.find(s => s.kind === kind);
const camera = choose("videoinput");
const microphone = choose("audioinput");
if (!camera || !microphone) throw new Error("Capture source unavailable");
return {
seed: "meeting-devices",
devices: [
{
key: "camera", kind: "videoinput",
source: { sourceId: camera.sourceId },
label: "Studio Camera", group: "desk",
modes: [{ width: 640, height: 480, frameRate: 30 }],
defaultMode: { width: 640, height: 480, frameRate: 30 },
processing: { resize: "crop-and-scale" },
},
{
key: "microphone", kind: "audioinput",
source: { sourceId: microphone.sourceId },
label: "Studio Microphone", group: "desk",
},
],
};
},
});
await context.setPermission("https://example.com",
{ permission: { name: "camera" }, state: "granted" },
{ permission: { name: "microphone" }, state: "granted" },
);
const page = await context.newPage();
await page.goto("https://example.com");
console.log(await page.evaluate(async () =>
(await navigator.mediaDevices.enumerateDevices()).map(device => device.toJSON())
));
} finally {
await session.close();
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});Run
npx tsx media.tsRequires available camera and microphone sources. The source supplies capture; labels and groups define the devices the site sees.
Create files
from mimic.playwright.sync_api import launch
from mimic.generated import GetMediaSourcesParams, MediaConfiguration, MediaDeviceProfile
with launch() as session:
def media(setup):
result = setup.mimic.get_media_sources(
GetMediaSourcesParams(browser_context_id=setup.browser_context_id)
)
def choose(kind):
sources = [source for source in result.sources if source.kind == kind]
return next((source for source in sources if source.default), sources[0] if sources else None)
camera = choose("videoinput")
microphone = choose("audioinput")
if camera is None or microphone is None:
raise RuntimeError("Capture source unavailable")
return MediaConfiguration(seed="meeting-devices", devices=[
MediaDeviceProfile(
key="camera", kind="videoinput", source={"sourceId": camera.source_id},
label="Studio Camera", group="desk",
modes=[{"width": 640, "height": 480, "frameRate": 30}],
default_mode={"width": 640, "height": 480, "frameRate": 30},
processing={"resize": "crop-and-scale"},
),
MediaDeviceProfile(
key="microphone", kind="audioinput", source={"sourceId": microphone.source_id},
label="Studio Microphone", group="desk",
),
])
context = session.new_context(media=media)
context.grant_permissions(["camera", "microphone"], origin="https://example.com")
page = context.new_page()
page.goto("https://example.com")
print(page.evaluate("async () => (await navigator.mediaDevices.enumerateDevices()).map(device => device.toJSON())"))Run
python media.pyRequires available camera and microphone sources. The source supplies capture; labels and groups define the devices the site sees.
Create files
import asyncio
from mimic.playwright.async_api import launch
from mimic.generated import GetMediaSourcesParams, MediaConfiguration, MediaDeviceProfile
async def main():
async with await launch() as session:
async def media(setup):
result = await setup.mimic.get_media_sources(
GetMediaSourcesParams(browser_context_id=setup.browser_context_id)
)
def choose(kind):
sources = [source for source in result.sources if source.kind == kind]
return next((source for source in sources if source.default), sources[0] if sources else None)
camera = choose("videoinput")
microphone = choose("audioinput")
if camera is None or microphone is None:
raise RuntimeError("Capture source unavailable")
return MediaConfiguration(seed="meeting-devices", devices=[
MediaDeviceProfile(
key="camera", kind="videoinput", source={"sourceId": camera.source_id},
label="Studio Camera", group="desk",
modes=[{"width": 640, "height": 480, "frameRate": 30}],
default_mode={"width": 640, "height": 480, "frameRate": 30},
processing={"resize": "crop-and-scale"},
),
MediaDeviceProfile(
key="microphone", kind="audioinput", source={"sourceId": microphone.source_id},
label="Studio Microphone", group="desk",
),
])
context = await session.new_context(media=media)
await context.grant_permissions(["camera", "microphone"], origin="https://example.com")
page = await context.new_page()
await page.goto("https://example.com")
print(await page.evaluate("async () => (await navigator.mediaDevices.enumerateDevices()).map(device => device.toJSON())"))
asyncio.run(main())Run
python media.pyRequires available camera and microphone sources. The source supplies capture; labels and groups define the devices the site sees.
Create files
import asyncio
from mimic.pyppeteer import launch
from mimic.generated import GetMediaSourcesParams, MediaConfiguration, MediaDeviceProfile
async def main():
async with await launch() as session:
async def media(setup):
result = await setup.mimic.get_media_sources(
GetMediaSourcesParams(browser_context_id=setup.browser_context_id)
)
def choose(kind):
sources = [source for source in result.sources if source.kind == kind]
return next((source for source in sources if source.default), sources[0] if sources else None)
camera = choose("videoinput")
microphone = choose("audioinput")
if camera is None or microphone is None:
raise RuntimeError("Capture source unavailable")
for name in ("camera", "microphone"):
await session.connection.call_async("Browser.setPermission", {
"browserContextId": setup.browser_context_id,
"permission": {"name": name}, "setting": "granted",
"origin": "https://example.com",
})
return MediaConfiguration(seed="meeting-devices", devices=[
MediaDeviceProfile(
key="camera", kind="videoinput", source={"sourceId": camera.source_id},
label="Studio Camera", group="desk",
modes=[{"width": 640, "height": 480, "frameRate": 30}],
default_mode={"width": 640, "height": 480, "frameRate": 30},
processing={"resize": "crop-and-scale"},
),
MediaDeviceProfile(
key="microphone", kind="audioinput", source={"sourceId": microphone.source_id},
label="Studio Microphone", group="desk",
),
])
context = await session.new_context(media=media)
page = await context.newPage()
await page.goto("https://example.com")
print(await page.evaluate("async () => (await navigator.mediaDevices.enumerateDevices()).map(device => device.toJSON())"))
asyncio.run(main())Run
python media.pyRequires accessible camera and microphone sources. Source IDs stay private; the page receives Studio Camera and Studio Microphone identities. This example opens capture and stops every track.
Create files
using Mimic.Playwright;
using Mimic.Sdk;
using Mimic.Sdk.Generated;
using System.Text.Json.Nodes;
await using var session = await PlaywrightSession.LaunchAsync();
var context = await session.NewContextAsync(mediaFactory: async (setup, token) =>
{
var result = await setup.Mimic.Commands.GetMediaSourcesAsync(
new() { BrowserContextId = Optional<string>.Of(setup.BrowserContextId) }, token);
var camera = result.Sources.FirstOrDefault(source => source.Kind == "videoinput")
?? throw new InvalidOperationException("No camera source is available");
var microphone = result.Sources.FirstOrDefault(source => source.Kind == "audioinput")
?? throw new InvalidOperationException("No microphone source is available");
var mode = new CameraFormat { Width = 1280, Height = 720, FrameRate = 30 };
return new MediaConfiguration
{
Devices = Optional<List<MediaDeviceProfile>>.Of([
new() {
Key = "camera", Kind = "videoinput", Label = "Studio Camera",
Source = new JsonObject { ["sourceId"] = camera.SourceId },
Group = Optional<string>.Of("desk"),
Modes = Optional<List<CameraFormat>>.Of([mode]),
DefaultMode = Optional<CameraFormat>.Of(mode),
Processing = Optional<MediaProcessing>.Of(new()
{ Resize = Optional<string>.Of("crop-and-scale") })
},
new() {
Key = "microphone", Kind = "audioinput", Label = "Studio Microphone",
Source = new JsonObject { ["sourceId"] = microphone.SourceId },
Group = Optional<string>.Of("desk")
}
])
};
});
var capabilities = await session.ForContextAsync(context);
await session.Mimic.SendAsync("Browser.grantPermissions", new JsonObject
{
["browserContextId"] = capabilities.Id,
["origin"] = "https://example.com",
["permissions"] = new JsonArray("videoCapture", "audioCapture")
});
var page = await context.NewPageAsync();
await page.GotoAsync("https://example.com");
Console.WriteLine(await page.EvaluateAsync<string>("""
async () => {
const devices = await navigator.mediaDevices.enumerateDevices();
const camera = devices.find(device => device.label === "Studio Camera");
const microphone = devices.find(device => device.label === "Studio Microphone");
if (!camera || !microphone) throw new Error("Configured media is unavailable");
const stream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: camera.deviceId } },
audio: { deviceId: { exact: microphone.deviceId } }
});
try {
return JSON.stringify(stream.getTracks().map(track => ({
kind: track.kind, label: track.label, state: track.readyState
})));
} finally {
stream.getTracks().forEach(track => track.stop());
}
}
"""));Run
dotnet runRequires accessible camera and microphone sources. Source IDs stay private; the page receives Studio Camera and Studio Microphone identities. This example opens capture and stops every track.
Create files
using Mimic.PuppeteerSharp;
using Mimic.Sdk;
using Mimic.Sdk.Generated;
using System.Text.Json.Nodes;
await using var session = await PuppeteerSession.LaunchAsync();
var context = await session.NewContextAsync(mediaFactory: async (setup, token) =>
{
var result = await setup.Mimic.Commands.GetMediaSourcesAsync(
new() { BrowserContextId = Optional<string>.Of(setup.BrowserContextId) }, token);
var camera = result.Sources.FirstOrDefault(source => source.Kind == "videoinput")
?? throw new InvalidOperationException("No camera source is available");
var microphone = result.Sources.FirstOrDefault(source => source.Kind == "audioinput")
?? throw new InvalidOperationException("No microphone source is available");
var mode = new CameraFormat { Width = 1280, Height = 720, FrameRate = 30 };
return new MediaConfiguration
{
Devices = Optional<List<MediaDeviceProfile>>.Of([
new() {
Key = "camera", Kind = "videoinput", Label = "Studio Camera",
Source = new JsonObject { ["sourceId"] = camera.SourceId },
Group = Optional<string>.Of("desk"),
Modes = Optional<List<CameraFormat>>.Of([mode]),
DefaultMode = Optional<CameraFormat>.Of(mode),
Processing = Optional<MediaProcessing>.Of(new()
{ Resize = Optional<string>.Of("crop-and-scale") })
},
new() {
Key = "microphone", Kind = "audioinput", Label = "Studio Microphone",
Source = new JsonObject { ["sourceId"] = microphone.SourceId },
Group = Optional<string>.Of("desk")
}
])
};
});
var capabilities = session.ForContext(context);
await session.Mimic.SendAsync("Browser.grantPermissions", new JsonObject
{
["browserContextId"] = capabilities.Id,
["origin"] = "https://example.com",
["permissions"] = new JsonArray("videoCapture", "audioCapture")
});
var page = await context.NewPageAsync();
await page.GoToAsync("https://example.com");
Console.WriteLine(await page.EvaluateFunctionAsync<string>("""
async () => {
const devices = await navigator.mediaDevices.enumerateDevices();
const camera = devices.find(device => device.label === "Studio Camera");
const microphone = devices.find(device => device.label === "Studio Microphone");
if (!camera || !microphone) throw new Error("Configured media is unavailable");
const stream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: camera.deviceId } },
audio: { deviceId: { exact: microphone.deviceId } }
});
try {
return JSON.stringify(stream.getTracks().map(track => ({
kind: track.kind, label: track.label, state: track.readyState
})));
} finally {
stream.getTracks().forEach(track => track.stop());
}
}
"""));Run
dotnet runRequires accessible camera and microphone sources. Source IDs stay private; capture stops every track.
Create files
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>mimic-demo</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>boo.mimic</groupId>
<artifactId>mimic-browser</artifactId>
<version>0.1.1</version>
</dependency>
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.63.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.6.1</version>
<configuration><mainClass>Main</mainClass></configuration>
</plugin>
</plugins>
</build>
</project>import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import io.mimicbrowser.sdk.Generated;
import io.mimicbrowser.sdk.RuntimeOptions;
import io.mimicbrowser.sdk.playwright.PlaywrightSession;
import java.util.List;
public class Main {
public static void main(String[] args) {
try (var session = PlaywrightSession.launch(new RuntimeOptions())) {
var context = session.newContext(null, null, null, setup -> {
var query = new Generated.GetMediaSourcesParams();
query.browserContextId = Generated.OptionalValue.of(setup.browserContextId());
var sources = setup.mimic().commands().getMediaSources(query).sources;
var camera = sources.stream().filter(s -> s.kind.equals("videoinput"))
.findFirst().orElseThrow(() -> new IllegalStateException("No camera source"));
var microphone = sources.stream().filter(s -> s.kind.equals("audioinput"))
.findFirst().orElseThrow(() -> new IllegalStateException("No microphone source"));
var video = device("camera", "videoinput", "Studio Camera", camera.sourceId);
var mode = new Generated.CameraFormat();
mode.width = 1280L;
mode.height = 720L;
mode.frameRate = 30.0;
video.modes = Generated.OptionalValue.of(List.of(mode));
video.defaultMode = Generated.OptionalValue.of(mode);
var processing = new Generated.MediaProcessing();
processing.resize = Generated.OptionalValue.of("crop-and-scale");
video.processing = Generated.OptionalValue.of(processing);
var audio = device("microphone", "audioinput", "Studio Microphone", microphone.sourceId);
var media = new Generated.MediaConfiguration();
media.devices = Generated.OptionalValue.of(List.of(video, audio));
return media;
});
var permission = new JsonObject();
permission.addProperty("browserContextId", session.forContext(context).id());
permission.addProperty("origin", "https://example.com");
var names = new JsonArray();
names.add("videoCapture");
names.add("audioCapture");
permission.add("permissions", names);
session.mimic().send("Browser.grantPermissions", permission);
var page = context.newPage();
page.navigate("https://example.com");
System.out.println(page.evaluate("""
async () => {
const devices = await navigator.mediaDevices.enumerateDevices();
const camera = devices.find(device => device.label === "Studio Camera");
const microphone = devices.find(device => device.label === "Studio Microphone");
if (!camera || !microphone) throw new Error("Configured media is unavailable");
const stream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: camera.deviceId } },
audio: { deviceId: { exact: microphone.deviceId } }
});
try {
return JSON.stringify(stream.getTracks().map(track => ({
kind: track.kind, label: track.label, state: track.readyState
})));
} finally {
stream.getTracks().forEach(track => track.stop());
}
}
"""));
}
}
private static Generated.MediaDeviceProfile device(
String key, String kind, String label, String sourceId) {
var result = new Generated.MediaDeviceProfile();
result.key = key;
result.kind = kind;
result.label = label;
var source = new JsonObject();
source.addProperty("sourceId", sourceId);
result.source = source;
result.group = Generated.OptionalValue.of("desk");
return result;
}
}Run
mvn -q compile exec:javaRequires accessible camera and microphone sources. Source IDs stay private; capture stops every track.
Create files
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>mimic-kotlin-demo</artifactId>
<version>1.0.0</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<kotlin.version>2.2.21</kotlin.version>
</properties>
<dependencies>
<dependency>
<groupId>boo.mimic</groupId>
<artifactId>mimic-browser</artifactId>
<version>0.1.1</version>
</dependency>
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.63.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>src/main/kotlin</sourceDirectory>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<configuration><jvmTarget>17</jvmTarget></configuration>
<executions>
<execution>
<id>compile</id><phase>compile</phase>
<goals><goal>compile</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.6.1</version>
<configuration><mainClass>MainKt</mainClass></configuration>
</plugin>
</plugins>
</build>
</project>import com.google.gson.JsonArray
import com.google.gson.JsonObject
import io.mimicbrowser.sdk.Generated
import io.mimicbrowser.sdk.RuntimeOptions
import io.mimicbrowser.sdk.playwright.PlaywrightSession
fun main() {
PlaywrightSession.launch(RuntimeOptions()).use { session ->
val context = session.newContext(null, null, null) { setup ->
val query = Generated.GetMediaSourcesParams().apply {
browserContextId = Generated.OptionalValue.of(setup.browserContextId())
}
val sources = setup.mimic().commands().getMediaSources(query).sources
val camera = sources.firstOrNull { it.kind == "videoinput" }
?: error("No camera source is available")
val microphone = sources.firstOrNull { it.kind == "audioinput" }
?: error("No microphone source is available")
val video = device("camera", "videoinput", "Studio Camera", camera.sourceId)
val mode = Generated.CameraFormat().apply {
width = 1280
height = 720
frameRate = 30.0
}
video.modes = Generated.OptionalValue.of(listOf(mode))
video.defaultMode = Generated.OptionalValue.of(mode)
video.processing = Generated.OptionalValue.of(Generated.MediaProcessing().apply {
resize = Generated.OptionalValue.of("crop-and-scale")
})
val audio = device("microphone", "audioinput", "Studio Microphone", microphone.sourceId)
Generated.MediaConfiguration().apply {
devices = Generated.OptionalValue.of(listOf(video, audio))
}
}
val permission = JsonObject().apply {
addProperty("browserContextId", session.forContext(context).id())
addProperty("origin", "https://example.com")
add("permissions", JsonArray().apply {
add("videoCapture")
add("audioCapture")
})
}
session.mimic().send("Browser.grantPermissions", permission)
val page = context.newPage()
page.navigate("https://example.com")
println(page.evaluate("""
async () => {
const devices = await navigator.mediaDevices.enumerateDevices();
const camera = devices.find(device => device.label === "Studio Camera");
const microphone = devices.find(device => device.label === "Studio Microphone");
if (!camera || !microphone) throw new Error("Configured media is unavailable");
const stream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: camera.deviceId } },
audio: { deviceId: { exact: microphone.deviceId } }
});
try {
return JSON.stringify(stream.getTracks().map(track => ({
kind: track.kind, label: track.label, state: track.readyState
})));
} finally {
stream.getTracks().forEach(track => track.stop());
}
}
""".trimIndent()))
}
}
fun device(key: String, kind: String, label: String, sourceId: String) =
Generated.MediaDeviceProfile().apply {
this.key = key
this.kind = kind
this.label = label
source = JsonObject().apply { addProperty("sourceId", sourceId) }
group = Generated.OptionalValue.of("desk")
}Run
mvn -q compile exec:javaUses your default camera and microphone, or the first available source of each kind. To select OBS, choose its video source by label. The site sees Studio Camera and Studio Microphone; only example.com receives capture permission, and every track is stopped.
Create files
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
"github.com/go-rod/rod/lib/proto"
mimic "github.com/mimic-browser/sdk/go"
sdk "github.com/mimic-browser/sdk/go/rod"
)
func main() {
if err := run(); err != nil { log.Fatal(err) }
}
func run() error {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
session, err := sdk.Launch(ctx, mimic.RuntimeOptions{})
if err != nil { return err }
defer session.Close()
browser, err := session.NewConfiguredContextWithMedia(ctx, mimic.ConfigureContextParams{}, mediaFor)
if err != nil { return err }
if err := session.Mimic.Call(ctx, "Browser.grantPermissions", map[string]any{
"browserContextId": string(browser.BrowserContextID), "origin": "https://example.com",
"permissions": []string{"videoCapture", "audioCapture"},
}, nil); err != nil { return err }
page, err := browser.Page(proto.TargetCreateTarget{URL: "https://example.com"})
if err != nil { return err }
if err := page.WaitLoad(); err != nil { return err }
result, err := page.Eval("() => " + `(async () => {
const devices = await navigator.mediaDevices.enumerateDevices();
const camera = devices.find(device => device.label === "Studio Camera");
const microphone = devices.find(device => device.label === "Studio Microphone");
if (!camera || !microphone) throw new Error("Configured devices unavailable");
const stream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: camera.deviceId } },
audio: { deviceId: { exact: microphone.deviceId } }
});
try {
return stream.getTracks().map(track => ({
label: track.label, settings: track.getSettings()
}));
} finally {
stream.getTracks().forEach(track => track.stop());
}
})()`)
if err != nil { return err }
fmt.Println(result.Value)
return nil
}
func mediaFor(ctx context.Context, setup mimic.ContextSetup) (mimic.MediaConfiguration, error) {
found, err := setup.Mimic.GetMediaSources(ctx, mimic.GetMediaSourcesParams{
BrowserContextId: mimic.Some(setup.BrowserContextID),
})
if err != nil { return mimic.MediaConfiguration{}, err }
var camera, microphone *mimic.MediaSource
for i := range found.Sources {
source := &found.Sources[i]
if source.Kind == "videoinput" && (camera == nil || source.Default) { camera = source }
if source.Kind == "audioinput" && (microphone == nil || source.Default) { microphone = source }
}
if camera == nil || microphone == nil {
return mimic.MediaConfiguration{}, fmt.Errorf("connect a camera and microphone before starting capture")
}
cameraSource, err := json.Marshal(map[string]string{"sourceId": camera.SourceId})
if err != nil { return mimic.MediaConfiguration{}, err }
microphoneSource, err := json.Marshal(map[string]string{"sourceId": microphone.SourceId})
if err != nil { return mimic.MediaConfiguration{}, err }
return mimic.MediaConfiguration{
Seed: mimic.Some("meeting-devices"),
Devices: mimic.Some([]mimic.MediaDeviceProfile{
{Key: "camera", Kind: "videoinput", Label: "Studio Camera",
Group: mimic.Some("desk"), Source: cameraSource,
Modes: mimic.Some([]mimic.CameraFormat{{Width: 1280, Height: 720, FrameRate: 30}}),
DefaultMode: mimic.Some(mimic.CameraFormat{Width: 1280, Height: 720, FrameRate: 30}),
Processing: mimic.Some(mimic.MediaProcessing{Resize: mimic.Some("crop-and-scale")})},
{Key: "microphone", Kind: "audioinput", Label: "Studio Microphone",
Group: mimic.Some("desk"), Source: microphoneSource},
}),
}, nil
}
Run
go run .Uses your default camera and microphone, or the first available source of each kind. To select OBS, choose its video source by label. The site sees Studio Camera and Studio Microphone; only example.com receives capture permission, and every track is stopped.
Create files
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
"github.com/chromedp/chromedp"
cdpruntime "github.com/chromedp/cdproto/runtime"
mimic "github.com/mimic-browser/sdk/go"
sdk "github.com/mimic-browser/sdk/go/chromedp"
)
func main() {
if err := run(); err != nil { log.Fatal(err) }
}
func run() error {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
var contextID string
factory := func(ctx context.Context, setup mimic.ContextSetup) (mimic.MediaConfiguration, error) {
contextID = setup.BrowserContextID
return mediaFor(ctx, setup)
}
session, err := sdk.LaunchConfiguredWithMedia(ctx, mimic.RuntimeOptions{}, mimic.ConfigureContextParams{}, factory)
if err != nil { return err }
defer session.Close()
if err := session.Mimic.Call(ctx, "Browser.grantPermissions", map[string]any{
"browserContextId": contextID, "origin": "https://example.com",
"permissions": []string{"videoCapture", "audioCapture"},
}, nil); err != nil { return err }
var tracks []map[string]any
if err := chromedp.Run(session.Context,
chromedp.Navigate("https://example.com"),
chromedp.Evaluate(`(async () => {
const devices = await navigator.mediaDevices.enumerateDevices();
const camera = devices.find(device => device.label === "Studio Camera");
const microphone = devices.find(device => device.label === "Studio Microphone");
if (!camera || !microphone) throw new Error("Configured devices unavailable");
const stream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: camera.deviceId } },
audio: { deviceId: { exact: microphone.deviceId } }
});
try {
return stream.getTracks().map(track => ({
label: track.label, settings: track.getSettings()
}));
} finally {
stream.getTracks().forEach(track => track.stop());
}
})()`, &tracks,
func(params *cdpruntime.EvaluateParams) *cdpruntime.EvaluateParams {
return params.WithAwaitPromise(true)
}),
); err != nil { return err }
fmt.Println(tracks)
return nil
}
func mediaFor(ctx context.Context, setup mimic.ContextSetup) (mimic.MediaConfiguration, error) {
found, err := setup.Mimic.GetMediaSources(ctx, mimic.GetMediaSourcesParams{
BrowserContextId: mimic.Some(setup.BrowserContextID),
})
if err != nil { return mimic.MediaConfiguration{}, err }
var camera, microphone *mimic.MediaSource
for i := range found.Sources {
source := &found.Sources[i]
if source.Kind == "videoinput" && (camera == nil || source.Default) { camera = source }
if source.Kind == "audioinput" && (microphone == nil || source.Default) { microphone = source }
}
if camera == nil || microphone == nil {
return mimic.MediaConfiguration{}, fmt.Errorf("connect a camera and microphone before starting capture")
}
cameraSource, err := json.Marshal(map[string]string{"sourceId": camera.SourceId})
if err != nil { return mimic.MediaConfiguration{}, err }
microphoneSource, err := json.Marshal(map[string]string{"sourceId": microphone.SourceId})
if err != nil { return mimic.MediaConfiguration{}, err }
return mimic.MediaConfiguration{
Seed: mimic.Some("meeting-devices"),
Devices: mimic.Some([]mimic.MediaDeviceProfile{
{Key: "camera", Kind: "videoinput", Label: "Studio Camera",
Group: mimic.Some("desk"), Source: cameraSource,
Modes: mimic.Some([]mimic.CameraFormat{{Width: 1280, Height: 720, FrameRate: 30}}),
DefaultMode: mimic.Some(mimic.CameraFormat{Width: 1280, Height: 720, FrameRate: 30}),
Processing: mimic.Some(mimic.MediaProcessing{Resize: mimic.Some("crop-and-scale")})},
{Key: "microphone", Kind: "audioinput", Label: "Studio Microphone",
Group: mimic.Some("desk"), Source: microphoneSource},
}),
}, nil
}
Run
go run .Uses your default camera and microphone, or the first available source of each kind. To select OBS, choose its video source by label. The site sees Studio Camera and Studio Microphone; only example.com receives capture permission, and every track is stopped.
Create files
use mimic_browser::{chromiumoxide::Session, RuntimeOptions, Error};
use mimic_browser::generated::{ConfigureContextParams, GetMediaSourcesParams, MediaConfiguration, WireOptional};
use chromiumoxide::cdp::browser_protocol::target::CreateTargetParams;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut session = Session::launch(RuntimeOptions::default()).await?;
let result: Result<(), Box<dyn std::error::Error>> = async {
let context = session.new_context_with_media(
ConfigureContextParams::default(),
|setup| async move {
let found = setup.mimic.get_media_sources(GetMediaSourcesParams {
browser_context_id: WireOptional::Value(setup.browser_context_id),
}).await?;
let camera = found.sources.iter().find(|source|
source.kind == "videoinput" && source.default
).or_else(|| found.sources.iter().find(|source| source.kind == "videoinput"))
.ok_or_else(|| Error::Invalid("No camera is available".into()))?;
let microphone = found.sources.iter().find(|source|
source.kind == "audioinput" && source.default
).or_else(|| found.sources.iter().find(|source| source.kind == "audioinput"))
.ok_or_else(|| Error::Invalid("No microphone is available".into()))?;
Ok(serde_json::from_value::<MediaConfiguration>(json!({
"seed": "meeting-devices",
"devices": [
{"key":"camera", "kind":"videoinput", "label":"Studio Camera",
"group":"desk",
"modes":[{"width":1280,"height":720,"frameRate":30}],
"defaultMode":{"width":1280,"height":720,"frameRate":30},
"processing":{"resize":"crop-and-scale"},
"source":{"sourceId":camera.source_id}},
{"key":"microphone", "kind":"audioinput", "label":"Studio Microphone",
"group":"desk", "source":{"sourceId":microphone.source_id}}
]
}))?)
},
).await?;
session.mimic.call("Browser.grantPermissions", Some(json!({
"browserContextId": context.as_ref(), "origin": "https://example.com",
"permissions": ["videoCapture", "audioCapture"]
}))).await?;
let page = session.browser.new_page(CreateTargetParams::builder()
.url("about:blank")
.browser_context_id(context)
.build()?
).await?;
page.goto("https://example.com").await?;
let tracks: serde_json::Value = page.evaluate(r#"(async () => {
const devices = await navigator.mediaDevices.enumerateDevices();
const camera = devices.find(device => device.label === "Studio Camera");
const microphone = devices.find(device => device.label === "Studio Microphone");
if (!camera || !microphone) throw new Error("Configured devices unavailable");
const stream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: camera.deviceId } },
audio: { deviceId: { exact: microphone.deviceId } }
});
try {
return stream.getTracks().map(track => ({
label: track.label, settings: track.getSettings()
}));
} finally {
stream.getTracks().forEach(track => track.stop());
}
})()"#).await?.into_value()?;
println!("{tracks}");
page.close().await?;
Ok(())
}.await;
let closed = session.close().await;
result?;
closed?;
Ok(())
}
Run
cargo runUses your default camera and microphone, or the first available source of each kind. To select OBS, choose its video source by label. The site sees Studio Camera and Studio Microphone; only example.com receives capture permission, and every track is stopped.
Create files
require "json"
require "mimic_sdk/ferrum"
MimicSDK::Ferrum.launch do |session|
context = session.new_context(media: lambda do |setup|
sources = setup.mimic.get_media_sources({
"browserContextId" => setup.browser_context_id
}).sources
cameras = sources.select { |source| source.kind == "videoinput" }
microphones = sources.select { |source| source.kind == "audioinput" }
camera = cameras.find(&:default) || cameras.first
microphone = microphones.find(&:default) || microphones.first
raise "Connect a camera and microphone before starting capture" unless camera && microphone
{
"seed" => "meeting-devices",
"devices" => [
{"key" => "camera", "kind" => "videoinput", "label" => "Studio Camera",
"group" => "desk",
"modes" => [{"width" => 1280, "height" => 720, "frameRate" => 30}],
"defaultMode" => {"width" => 1280, "height" => 720, "frameRate" => 30},
"processing" => {"resize" => "crop-and-scale"},
"source" => {"sourceId" => camera.source_id}},
{"key" => "microphone", "kind" => "audioinput", "label" => "Studio Microphone",
"group" => "desk", "source" => {"sourceId" => microphone.source_id}}
]
}
end)
session.mimic.call("Browser.grantPermissions", {
"browserContextId" => context.id, "origin" => "https://example.com",
"permissions" => ["videoCapture", "audioCapture"]
})
page = context.create_page
page.go_to("https://example.com")
observed = page.evaluate_async(<<~JS, 15)
const done = arguments[0];
(async () => {
const devices = await navigator.mediaDevices.enumerateDevices();
const camera = devices.find(device => device.label === "Studio Camera");
const microphone = devices.find(device => device.label === "Studio Microphone");
if (!camera || !microphone) throw new Error("Configured devices unavailable");
const stream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: camera.deviceId } },
audio: { deviceId: { exact: microphone.deviceId } }
});
try {
return stream.getTracks().map(track => ({
label: track.label, settings: track.getSettings()
}));
} finally {
stream.getTracks().forEach(track => track.stop());
}
})().then(tracks => done({tracks}))
.catch(error => done({error: error.message}));
JS
raise observed.fetch("error") if observed.key?("error")
puts JSON.pretty_generate(observed.fetch("tracks"))
end
Run
ruby scrape.rbRequires accessible camera and microphone sources. Source IDs stay private; capture stops every track.
Create files
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Mimic\Sdk\Chrome\ChromeSession;
use Mimic\Sdk\ContextSetup;
use Mimic\Sdk\Generated\GetMediaSourcesParams;
use Mimic\Sdk\Generated\MediaConfiguration;
$session = ChromeSession::launch();
try {
$context = $session->newContext(mediaFactory: function (ContextSetup $setup): MediaConfiguration {
$query = new GetMediaSourcesParams();
$query->browserContextId = $setup->browserContextId;
$sources = $setup->mimic->commands->getMediaSources($query)->sources;
$cameras = array_values(array_filter($sources, fn($s) => $s->kind === 'videoinput'));
$microphones = array_values(array_filter($sources, fn($s) => $s->kind === 'audioinput'));
if (!$cameras || !$microphones) {
throw new RuntimeException('A camera and microphone source are required');
}
return MediaConfiguration::fromWire(['devices' => [
[
'key' => 'camera', 'kind' => 'videoinput',
'source' => ['sourceId' => $cameras[0]->sourceId],
'label' => 'Studio Camera', 'group' => 'desk',
'modes' => [['width' => 1280, 'height' => 720, 'frameRate' => 30]],
'defaultMode' => ['width' => 1280, 'height' => 720, 'frameRate' => 30],
'processing' => ['resize' => 'crop-and-scale'],
],
[
'key' => 'microphone', 'kind' => 'audioinput',
'source' => ['sourceId' => $microphones[0]->sourceId],
'label' => 'Studio Microphone', 'group' => 'desk',
],
]]);
});
$session->mimic->send('Browser.grantPermissions', [
'browserContextId' => $context->id,
'origin' => 'https://example.com',
'permissions' => ['videoCapture', 'audioCapture'],
]);
$page = $session->newPage($context);
$page->navigate('https://example.com')->waitForNavigation();
echo $page->evaluate(<<<'JS'
(async () => {
const devices = await navigator.mediaDevices.enumerateDevices();
const camera = devices.find(device => device.label === "Studio Camera");
const microphone = devices.find(device => device.label === "Studio Microphone");
if (!camera || !microphone) throw new Error("Configured media is unavailable");
const stream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: camera.deviceId } },
audio: { deviceId: { exact: microphone.deviceId } }
});
try {
return JSON.stringify(stream.getTracks().map(track => ({
kind: track.kind, label: track.label, state: track.readyState
})));
} finally {
stream.getTracks().forEach(track => track.stop());
}
})()
JS
)->getReturnValue(), PHP_EOL;
} finally {
$session->close();
}Run
php example.phpChoose the input; define the public device
A capture source provides actual frames or audio samples. A media profile defines the logical devices exposed by enumerateDevices(), track labels, settings, and camera capabilities. Changing a public label does not change which source supplies the data.
Call Mimic.getMediaSources for the new Context inside the media factory. Discovery returns a private label, kind, default flag, and opaque sourceId; it does not open a camera or microphone. Use that ID in the device's source selector. For a chosen native label, require exactly one match and report a missing or ambiguous source.
Treat source IDs as temporary, Context-bound handles. Discover them again for a new Context or runtime; do not save them as portable device identifiers. They are different from the origin-scoped deviceId returned to web content. Native labels, paths, and source IDs stay in the automation interface; configured pages receive the public device identity.
key- A unique logical name such as
cameraormicrophonewithin your configured catalog. label- The public device and track name. Permission-dependent label redaction still applies before capture permission is granted.
group- Give a camera and microphone the same group to associate their public identities. If omitted, the group defaults to the device key.
seed- Makes generated recipe selection repeatable. It does not make web device IDs interchangeable between origins or Contexts, and it does not preserve a private source binding across runtime launches.
A configured catalog exposes only its listed inputs. An empty devices array exposes no capture inputs; leaving media unconfigured retains native enumeration. A missing configured source is omitted rather than silently replaced with an unlisted device.
Configure before creating your page
The SDK's Context helper accepts a media configuration or a factory. Use a factory when selecting discovered sources: it receives the new browser Context ID and a Mimic command client, then returns the media configuration before any application Page is created. The framework's own Context and Page objects remain the ones your application uses.
- Create the Context through the SDK helper.
- Discover sources for that Context inside the factory.
- Return the complete public device catalog.
- Grant permission for the intended origin, then open the page and capture.
Always supply browserContextId when issuing media commands for a non-default Context. Omitting it selects the runtime's default Context, even when the command is sent through a Page session. Source discovery and profile validation perform no capture. If a factory or configuration fails, the SDK disposes the Context it just created.
Grant permission to the page's origin
Defining a media profile does not grant camera or microphone access. The examples use Browser.grantPermissions with the new browserContextId, the exact origin, and videoCapture / audioCapture. Change the origin together with the page URL when adapting the example to your application.
Use an HTTPS page or a trustworthy local origin for navigator.mediaDevices. Wait for navigation before evaluating capture code. Unset or denied permissions reject immediately in automation; no interactive permission prompt is required. Revoking permission ends the affected tracks and their clones.
Set camera output modes
Every explicit camera in devices needs a nonempty modes list and a defaultMode that exactly matches one of its entries. Add the following fields to the camera device to offer VGA and HD output, with HD selected by default:
{
"modes": [
{ "width": 640, "height": 480, "frameRate": 30 },
{ "width": 1280, "height": 720, "frameRate": 30 }
],
"defaultMode": { "width": 1280, "height": 720, "frameRate": 30 },
"processing": { "resize": "crop-and-scale" }
}processing.resize: "crop-and-scale" lets Mimic produce the declared output by cropping or scaling native frames. Without it, a custom camera recipe must use a single mode with supported native geometry. An explicit device's optional profile bounds its supplied recipe; it does not fill in missing modes. The separate camera shorthand expands presets automatically. Choose either shorthand or devices in a configuration, never both.
Profile validation checks the recipe without opening hardware and returns nativeModesVerified: false. Actual source support is checked when capture starts. The source must sustain the recipe's maximum nominal frame rate; scaling a frame adds no detail, and Mimic does not duplicate frames to manufacture a higher capture rate.
A catalog can contain up to 16 devices and each camera up to 64 modes. Dimensions must be integers from 1 to 8192, with at most 16 megapixels per frame; frame rates must be from 1 to 120 FPS. These are recipe limits, not a guarantee that a particular source can supply every accepted mode.
Microphones accept source, identity, and grouping fields. Camera modes and video processing do not apply to audio inputs. Audio settings reflect the capture backend; a public microphone label does not add echo cancellation, noise suppression, or other signal processing.
Request and inspect the actual output
Use ordinary getUserMedia() constraints to select a public device and output, then inspect track.getSettings(). The examples request exact public device IDs so capture cannot choose a different listed input. Required output constraints must be feasible; ideal values express a preference among feasible outputs.
track.applyConstraints() uses the same output selection rules. Impossible requests reject with OverconstrainedError and preserve the previous settings and output. A clone shares its native input but can select a different supported output. A declared base geometry reports resizeMode: "none"; an adapted geometry reports "crop-and-scale", even when processing is enabled for both.
Facing direction is not inferred. Required facingMode, zoom, focus, exposure, white balance, torch, PTZ, and ImageCapture controls are outside the supported camera contract. Generic presets describe physical device classes; they do not certify the behavior of a particular camera model or driver.
Stop capture before replacing the catalog
Stop every track in a finally block, including clones your application created. Then close the owned Page or Context and the SDK session. Closing a session created with connect leaves the shared runtime running; a session created with launch also owns its runtime process.
Catalog replacement is atomic and rejects while capture in that Context is opening, active, or finishing cleanup. A rejected replacement preserves the old catalog. Wait for capture teardown before configuring it again. Navigation closes the departing document's capture while retaining the Context's catalog for subsequent pages.
When capture fails
- No matching source: inspect discovery results from the same Context. Confirm the camera or virtual camera is running on the runtime's machine, and use the actual input label when selecting a specific source.
- Profile rejected: check unique keys, source selectors, complete camera modes, and an exact matching default mode. Remove video processing fields from microphone devices.
- Media API unavailable or permission denied: wait for the correct secure page to load and grant permission to its origin in the same Context.
- Capture cannot start: validation does not prove native mode support. Read private source diagnostics and
Mimic.getMediaProfilediagnostics through the automation client; web-visible errors intentionally omit private backend details. - Profile update rejected: stop every original and cloned track, let backend cleanup finish, and retry only after capture is closed.
Continue with the SDK reference for typed media commands, or Contexts and ownership to combine media with an environment profile and resource settings.