Skip to content

Commit c2e6eba

Browse files
authored
Merge pull request #38 from eatsjobs/fix/stream-labels
fix/stream labels
2 parents 3ea3b98 + 66857c5 commit c2e6eba

5 files changed

Lines changed: 280 additions & 22 deletions

File tree

.changeset/seven-forks-cough.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@eatsjobs/media-mock": minor
3+
---
4+
5+
The media track labels and id have now the devices ones

lib/main.ts

Lines changed: 102 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -720,31 +720,56 @@ export class MediaMockClass {
720720

721721
const videoTracks = canvasStream?.getVideoTracks() ?? [];
722722

723-
// Normalize MediaStreamTrack methods to provide consistent real-device behavior
724-
videoTracks.forEach((track) => {
723+
// We detect the facing mode from constraints and get the video device based on that
724+
// then we override the id and label based on that
725+
const facingMode = this.getFacingModeFromConstraints(constraints);
726+
const videoDevice = this.getDeviceForFacingMode(
727+
facingMode,
728+
this.settings.device,
729+
);
730+
731+
videoTracks.forEach((track: MediaStreamTrack) => {
732+
// Set the track label to match the selected device label
733+
if (videoDevice?.label) {
734+
Object.defineProperty(track, "label", {
735+
value: videoDevice.label,
736+
writable: false,
737+
configurable: false,
738+
});
739+
}
740+
741+
// Set the track id (deviceId) to match the selected device
742+
if (videoDevice?.deviceId) {
743+
Object.defineProperty(track, "id", {
744+
value: videoDevice.deviceId,
745+
writable: false,
746+
configurable: false,
747+
});
748+
}
749+
725750
// Ensure getCapabilities method is always available (all real devices have this)
726751
if (!track.getCapabilities) {
727-
// Find the first video input device to get its capabilities
728-
const videoDevice = this.settings.device.mediaDeviceInfo.find(
729-
(device) => device.kind === "videoinput",
730-
);
731-
732752
if (videoDevice?.getCapabilities) {
733753
// Use the device-specific capabilities from mockCapabilities
734-
track.getCapabilities = () => videoDevice.getCapabilities();
754+
// Bind to track so 'this' refers to the track
755+
track.getCapabilities = function (this: MediaStreamTrack) {
756+
return videoDevice.getCapabilities();
757+
}.bind(track);
735758
} else {
736759
// Fallback to device resolutions if no specific capabilities defined
737760
const deviceResolutions = this.settings.device.videoResolutions;
738761
const widths = deviceResolutions.map((res) => res.width);
739762
const heights = deviceResolutions.map((res) => res.height);
740763

741-
track.getCapabilities = () => ({
742-
width: { min: Math.min(...widths), max: Math.max(...widths) },
743-
height: { min: Math.min(...heights), max: Math.max(...heights) },
744-
frameRate: { min: 1, max: 60 },
745-
facingMode: ["user", "environment"],
746-
resizeMode: ["none", "crop-and-scale"],
747-
});
764+
track.getCapabilities = function (this: MediaStreamTrack) {
765+
return {
766+
width: { min: Math.min(...widths), max: Math.max(...widths) },
767+
height: { min: Math.min(...heights), max: Math.max(...heights) },
768+
frameRate: { min: 1, max: 60 },
769+
facingMode: ["user", "environment"],
770+
resizeMode: ["none", "crop-and-scale"],
771+
};
772+
}.bind(track);
748773
}
749774
}
750775

@@ -784,6 +809,68 @@ export class MediaMockClass {
784809
return 30;
785810
}
786811

812+
/**
813+
* Extract facingMode from constraints (can be a string or ConstrainDOMString)
814+
*/
815+
private getFacingModeFromConstraints(
816+
constraints: MediaStreamConstraints,
817+
): string | null {
818+
if (typeof constraints.video === "object" && constraints.video.facingMode) {
819+
const facingMode = constraints.video.facingMode;
820+
if (typeof facingMode === "string") {
821+
return facingMode;
822+
}
823+
// facingMode can be an object with ideal/exact properties
824+
const facingModeObj = facingMode as Record<string, unknown>;
825+
if (facingModeObj.ideal) {
826+
const ideal = facingModeObj.ideal;
827+
return Array.isArray(ideal) ? ideal[0] : (ideal as string);
828+
}
829+
if (facingModeObj.exact) {
830+
const exact = facingModeObj.exact;
831+
return Array.isArray(exact) ? exact[0] : (exact as string);
832+
}
833+
}
834+
return null;
835+
}
836+
837+
/**
838+
* Get the appropriate camera device based on facingMode
839+
* Falls back to last videoinput if no matching camera found
840+
*/
841+
private getDeviceForFacingMode(
842+
facingMode: string | null,
843+
device: DeviceConfig,
844+
): MockMediaDeviceInfo | undefined {
845+
const videoDevices = device.mediaDeviceInfo.filter(
846+
(d) => d.kind === "videoinput",
847+
);
848+
849+
if (!videoDevices.length) {
850+
return undefined;
851+
}
852+
853+
if (!facingMode) {
854+
return videoDevices[0];
855+
}
856+
857+
// Find all devices that support the requested facingMode and return the last one
858+
// This usually is the Back Camera or camera2 0, facing back for the given default devices
859+
const matchingDevices = videoDevices.filter((d) => {
860+
const capabilities = d.getCapabilities();
861+
const supportedFacingModes = capabilities.facingMode;
862+
return (
863+
Array.isArray(supportedFacingModes) &&
864+
supportedFacingModes.includes(facingMode)
865+
);
866+
});
867+
868+
// Return the last matching device if found, otherwise fall back to first videoinput
869+
return matchingDevices.length > 0
870+
? matchingDevices[matchingDevices.length - 1]
871+
: videoDevices[0];
872+
}
873+
787874
/**
788875
* Get the appropriate resolution based on device orientation and constraints
789876
* @param constraints Media constraints

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ async function startStream() {
44
const videoElement = document.querySelector<HTMLVideoElement>(
55
"#video",
66
) as HTMLVideoElement;
7-
// const assetURL = "/assets/ean8_12345670.png"; // https://www.pexels.com/video/signing-the-parcel-4440957/
7+
// const assetURL = "/assets/ean8_12345670.png";
8+
// https://www.pexels.com/video/signing-the-parcel-4440957/
89
const videoAssetURL = "/assets/hd_1280_720_25fps.mp4";
910
MediaMock.setMockedVideoTracksHandler((tracks) => {
1011
const capabilities = tracks[0].getCapabilities();
11-
tracks[0].getCapabilities = () => ({
12-
...capabilities,
13-
whatever: 1,
14-
});
12+
tracks[0].getCapabilities = function (
13+
this: MediaStreamTrack,
14+
): MediaTrackCapabilities & { whatever: number } {
15+
return {
16+
...capabilities,
17+
whatever: 1,
18+
};
19+
}.bind(tracks[0]);
1520
return tracks;
1621
}).mock(devices["iPhone 12"]);
1722

@@ -20,12 +25,17 @@ async function startStream() {
2025
try {
2126
const stream = await navigator.mediaDevices.getUserMedia({
2227
video: {
28+
facingMode: { exact: "environment" },
2329
width: { ideal: 1920 },
2430
height: { ideal: 1080 },
2531
frameRate: { ideal: 5 },
2632
},
2733
});
2834

35+
stream.getVideoTracks().forEach((track) => {
36+
console.log("Track:", track.label, track.id);
37+
});
38+
2939
videoElement.srcObject = stream;
3040

3141
await videoElement.play();

tests/main.test.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,4 +1253,160 @@ describe("MediaMock", () => {
12531253
expect(settings.width).toBeGreaterThan(0);
12541254
expect(settings.height).toBeGreaterThan(0);
12551255
});
1256+
1257+
// ===== STREAM LABEL TESTS =====
1258+
1259+
it("should set stream track label to match selected device label", async () => {
1260+
const device = getDeviceForBrowser();
1261+
MediaMock.mock(device);
1262+
await MediaMock.setMediaURL(imageUrl);
1263+
1264+
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
1265+
expect(stream).toBeDefined();
1266+
expect(stream.getVideoTracks().length).toBeGreaterThan(0);
1267+
1268+
const videoTrack = stream.getVideoTracks()[0];
1269+
1270+
// Get the expected label from the device's first videoinput device
1271+
const expectedLabel = device.mediaDeviceInfo.find(
1272+
(d) => d.kind === "videoinput",
1273+
)?.label;
1274+
1275+
// Stream track label should match the device label
1276+
expect(videoTrack.label).toBe(expectedLabel);
1277+
});
1278+
1279+
it("should select front camera when facingMode user constraint is specified", async () => {
1280+
const device = getDeviceForBrowser();
1281+
MediaMock.mock(device);
1282+
await MediaMock.setMediaURL(imageUrl);
1283+
1284+
const stream = await navigator.mediaDevices.getUserMedia({
1285+
video: { facingMode: "user" },
1286+
});
1287+
expect(stream).toBeDefined();
1288+
1289+
const videoTrack = stream.getVideoTracks()[0];
1290+
1291+
// Get the last front camera (user) from the device (should be the primary one)
1292+
const frontCameras = device.mediaDeviceInfo.filter(
1293+
(d) => d.kind === "videoinput" && d.getCapabilities().facingMode?.includes("user"),
1294+
);
1295+
const primaryFrontCamera = frontCameras.length > 0 ? frontCameras[frontCameras.length - 1] : undefined;
1296+
1297+
// Stream track label should match the selected front camera label
1298+
expect(videoTrack.label).toBe(primaryFrontCamera?.label);
1299+
});
1300+
1301+
it("should select back camera when facingMode environment constraint is specified", async () => {
1302+
const device = getDeviceForBrowser();
1303+
MediaMock.mock(device);
1304+
await MediaMock.setMediaURL(imageUrl);
1305+
1306+
const stream = await navigator.mediaDevices.getUserMedia({
1307+
video: { facingMode: "environment" },
1308+
});
1309+
expect(stream).toBeDefined();
1310+
1311+
const videoTrack = stream.getVideoTracks()[0];
1312+
1313+
// Get the last back camera (environment) from the device (should be the primary one)
1314+
const backCameras = device.mediaDeviceInfo.filter(
1315+
(d) => d.kind === "videoinput" && d.getCapabilities().facingMode?.includes("environment"),
1316+
);
1317+
const primaryBackCamera = backCameras.length > 0 ? backCameras[backCameras.length - 1] : undefined;
1318+
1319+
// If back camera exists, verify the label matches
1320+
if (primaryBackCamera) {
1321+
expect(videoTrack.label).toBe(primaryBackCamera.label);
1322+
}
1323+
});
1324+
1325+
it("should set stream track deviceId to match selected device", async () => {
1326+
const device = getDeviceForBrowser();
1327+
MediaMock.mock(device);
1328+
await MediaMock.setMediaURL(imageUrl);
1329+
1330+
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
1331+
expect(stream).toBeDefined();
1332+
expect(stream.getVideoTracks().length).toBeGreaterThan(0);
1333+
1334+
const videoTrack = stream.getVideoTracks()[0];
1335+
1336+
// Get the expected deviceId from the device's first videoinput device
1337+
const expectedDeviceId = device.mediaDeviceInfo.find(
1338+
(d) => d.kind === "videoinput",
1339+
)?.deviceId;
1340+
1341+
// Stream track deviceId should match the device deviceId
1342+
expect(videoTrack.id).toBe(expectedDeviceId);
1343+
});
1344+
1345+
it("should set stream track deviceId based on facingMode constraint", async () => {
1346+
const device = getDeviceForBrowser();
1347+
MediaMock.mock(device);
1348+
await MediaMock.setMediaURL(imageUrl);
1349+
1350+
const stream = await navigator.mediaDevices.getUserMedia({
1351+
video: { facingMode: "user" },
1352+
});
1353+
expect(stream).toBeDefined();
1354+
1355+
const videoTrack = stream.getVideoTracks()[0];
1356+
1357+
// Get the last front camera (user) from the device (should be the primary one)
1358+
const frontCameras = device.mediaDeviceInfo.filter(
1359+
(d) => d.kind === "videoinput" && d.getCapabilities().facingMode?.includes("user"),
1360+
);
1361+
const primaryFrontCamera = frontCameras.length > 0 ? frontCameras[frontCameras.length - 1] : undefined;
1362+
1363+
// Stream track deviceId should match the selected camera's deviceId
1364+
expect(videoTrack.id).toBe(primaryFrontCamera?.deviceId);
1365+
});
1366+
1367+
it("should support facingMode as object with ideal constraint", async () => {
1368+
const device = getDeviceForBrowser();
1369+
MediaMock.mock(device);
1370+
await MediaMock.setMediaURL(imageUrl);
1371+
1372+
const stream = await navigator.mediaDevices.getUserMedia({
1373+
video: { facingMode: { ideal: "environment" } },
1374+
});
1375+
expect(stream).toBeDefined();
1376+
1377+
const videoTrack = stream.getVideoTracks()[0];
1378+
1379+
// Get the last back camera (environment) from the device
1380+
const backCameras = device.mediaDeviceInfo.filter(
1381+
(d) => d.kind === "videoinput" && d.getCapabilities().facingMode?.includes("environment"),
1382+
);
1383+
const primaryBackCamera = backCameras.length > 0 ? backCameras[backCameras.length - 1] : undefined;
1384+
1385+
// Stream track label should match the selected back camera
1386+
if (primaryBackCamera) {
1387+
expect(videoTrack.label).toBe(primaryBackCamera.label);
1388+
}
1389+
});
1390+
1391+
it("should support facingMode as object with exact constraint", async () => {
1392+
const device = getDeviceForBrowser();
1393+
MediaMock.mock(device);
1394+
await MediaMock.setMediaURL(imageUrl);
1395+
1396+
const stream = await navigator.mediaDevices.getUserMedia({
1397+
video: { facingMode: { exact: "user" } },
1398+
});
1399+
expect(stream).toBeDefined();
1400+
1401+
const videoTrack = stream.getVideoTracks()[0];
1402+
1403+
// Get the last front camera (user) from the device
1404+
const frontCameras = device.mediaDeviceInfo.filter(
1405+
(d) => d.kind === "videoinput" && d.getCapabilities().facingMode?.includes("user"),
1406+
);
1407+
const primaryFrontCamera = frontCameras.length > 0 ? frontCameras[frontCameras.length - 1] : undefined;
1408+
1409+
// Stream track label should match the selected front camera
1410+
expect(videoTrack.label).toBe(primaryFrontCamera?.label);
1411+
});
12561412
});

0 commit comments

Comments
 (0)