@@ -13,6 +13,7 @@ import { isPictureInPictureSupported } from "./media/controls";
1313import { getRTCPeerConnectionConstructor } from "./webrtc/shared" ;
1414import {
1515 attachMediaStreamToPeerConnection ,
16+ createMirroredVideoTrack ,
1617 createNewWHIP ,
1718 getDisplayMedia ,
1819 getDisplayMediaExists ,
@@ -146,6 +147,14 @@ export type InitialBroadcastProps = {
146147 * Set to true to disable ICE gathering. This is useful for testing purposes.
147148 */
148149 noIceGathering ?: boolean ;
150+
151+ /**
152+ * Whether the video stream should be mirrored (horizontally flipped).
153+ *
154+ * Set to true to broadcast a mirrored view.
155+ * Defaults to `false`.
156+ */
157+ mirrored ?: boolean ;
149158} ;
150159
151160export type BroadcastAriaText = {
@@ -326,6 +335,7 @@ export const createBroadcastStore = ({
326335 ingestUrl : ingestUrl ?? null ,
327336 video : initialProps ?. video ?? true ,
328337 noIceGathering : initialProps ?. noIceGathering ?? false ,
338+ mirrored : initialProps ?. mirrored ?? false ,
329339 } ,
330340
331341 __device : device ,
@@ -638,7 +648,11 @@ export const addBroadcastEventListeners = (
638648 }
639649
640650 // add effects
641- const removeEffectsFromStore = addEffectsToStore ( element , store , mediaStore ) ;
651+ const { destroy : destroyEffects } = addEffectsToStore (
652+ element ,
653+ store ,
654+ mediaStore ,
655+ ) ;
642656
643657 const removeHydrationListener = store . persist . onFinishHydration (
644658 ( { mediaDeviceIds, audio, video } ) => {
@@ -656,7 +670,7 @@ export const addBroadcastEventListeners = (
656670
657671 mediaDevices ?. removeEventListener ?.( "devicechange" , onDeviceChange ) ;
658672
659- removeEffectsFromStore ?.( ) ;
673+ destroyEffects ?.( ) ;
660674
661675 element ?. removeAttribute ?.( MEDIA_BROADCAST_INITIALIZED_ATTRIBUTE ) ;
662676 } ,
@@ -818,6 +832,7 @@ const addEffectsToStore = (
818832 requestedVideoDeviceId : state . __controls . requestedVideoInputDeviceId ,
819833 initialAudioConfig : state . __initialProps . audio ,
820834 initialVideoConfig : state . __initialProps . video ,
835+ mirrored : state . __initialProps . mirrored ,
821836 previousMediaStream : state . mediaStream ,
822837 } ) ,
823838 async ( {
@@ -830,6 +845,7 @@ const addEffectsToStore = (
830845 previousMediaStream,
831846 initialAudioConfig,
832847 initialVideoConfig,
848+ mirrored,
833849 } ) => {
834850 try {
835851 if ( ! mounted || ! hydrated ) {
@@ -871,7 +887,6 @@ const addEffectsToStore = (
871887 ? {
872888 ...( audioConstraints ? audioConstraints : { } ) ,
873889 deviceId : {
874- // we pass ideal here, so we don't get overconstrained errors
875890 ideal : requestedAudioDeviceId ,
876891 } ,
877892 }
@@ -887,13 +902,14 @@ const addEffectsToStore = (
887902 ? {
888903 ...( videoConstraints ? videoConstraints : { } ) ,
889904 deviceId : {
890- // we pass ideal here, so we don't get overconstrained errors
891905 ideal : requestedVideoDeviceId ,
892906 } ,
907+ ...( mirrored ? { facingMode : "user" } : { } ) ,
893908 }
894909 : video
895910 ? {
896911 ...( videoConstraints ? videoConstraints : { } ) ,
912+ ...( mirrored ? { facingMode : "user" } : { } ) ,
897913 }
898914 : false ,
899915 } ) ) ;
@@ -934,11 +950,38 @@ const addEffectsToStore = (
934950 allAudioTracks ?. [ 0 ] ??
935951 previousMediaStream ?. getAudioTracks ?.( ) ?. [ 0 ] ??
936952 null ;
937- const mergedVideoTrack =
953+
954+ let mergedVideoTrack =
938955 allVideoTracks ?. [ 0 ] ??
939956 previousMediaStream ?. getVideoTracks ?.( ) ?. [ 0 ] ??
940957 null ;
941958
959+ if (
960+ mergedVideoTrack &&
961+ mirrored &&
962+ requestedVideoDeviceId !== "screen"
963+ ) {
964+ try {
965+ const videoSettings = mergedVideoTrack . getSettings ( ) ;
966+ const isFrontFacing =
967+ videoSettings . facingMode === "user" ||
968+ ! videoSettings . facingMode ;
969+
970+ if ( isFrontFacing ) {
971+ element . classList . add ( "livepeer-mirrored-video" ) ;
972+ mergedVideoTrack = createMirroredVideoTrack ( mergedVideoTrack ) ;
973+ } else {
974+ element . classList . remove ( "livepeer-mirrored-video" ) ;
975+ }
976+ } catch ( err ) {
977+ warn (
978+ `Failed to apply video mirroring: ${ ( err as Error ) . message } ` ,
979+ ) ;
980+ }
981+ } else {
982+ element . classList . remove ( "livepeer-mirrored-video" ) ;
983+ }
984+
942985 if ( mergedAudioTrack ) mergedMediaStream . addTrack ( mergedAudioTrack ) ;
943986 if ( mergedVideoTrack ) mergedMediaStream . addTrack ( mergedVideoTrack ) ;
944987
@@ -1123,20 +1166,22 @@ const addEffectsToStore = (
11231166 } ,
11241167 ) ;
11251168
1126- return ( ) => {
1127- destroyAudioVideoEnabled ?.( ) ;
1128- destroyErrorCount ?.( ) ;
1129- destroyMapDeviceListToFriendly ?.( ) ;
1130- destroyMediaStream ?.( ) ;
1131- destroyMediaSyncError ?.( ) ;
1132- destroyMediaSyncMounted ?.( ) ;
1133- destroyPeerConnectionAndMediaStream ?.( ) ;
1134- destroyPictureInPictureSupportedMonitor ?.( ) ;
1135- destroyRequestUserMedia ?.( ) ;
1136- destroyUpdateDeviceList ?.( ) ;
1137- destroyWhip ?.( ) ;
1138-
1139- cleanupWhip ?.( ) ;
1140- cleanupMediaStream ?.( ) ;
1169+ return {
1170+ destroy : ( ) => {
1171+ destroyAudioVideoEnabled ?.( ) ;
1172+ destroyErrorCount ?.( ) ;
1173+ destroyMapDeviceListToFriendly ?.( ) ;
1174+ destroyMediaStream ?.( ) ;
1175+ destroyMediaSyncError ?.( ) ;
1176+ destroyMediaSyncMounted ?.( ) ;
1177+ destroyPeerConnectionAndMediaStream ?.( ) ;
1178+ destroyPictureInPictureSupportedMonitor ?.( ) ;
1179+ destroyRequestUserMedia ?.( ) ;
1180+ destroyUpdateDeviceList ?.( ) ;
1181+ destroyWhip ?.( ) ;
1182+
1183+ cleanupWhip ?.( ) ;
1184+ cleanupMediaStream ?.( ) ;
1185+ } ,
11411186 } ;
11421187} ;
0 commit comments