@@ -37,6 +37,8 @@ import { objectExpression } from '@babel/types';
3737import { DirectLineStreaming } from './directLineStreaming' ;
3838export { DirectLineStreaming } ;
3939
40+ import { hasIframeMicrophonePermission , isInIframe } from './iframeMicrophone' ;
41+
4042const DIRECT_LINE_VERSION = 'DirectLine/3.0' ;
4143
4244declare var process : {
@@ -458,52 +460,6 @@ const konsole = {
458460 }
459461}
460462
461- /**
462- * Checks if the current context is running inside an iframe.
463- */
464- const isInIframe = ( ) : boolean => {
465- try {
466- return typeof window !== 'undefined' && window . self !== window . top ;
467- } catch ( e ) {
468- // If accessing window.top throws (cross-origin), we're definitely in an iframe
469- return true ;
470- }
471- }
472-
473- /**
474- * Checks if the iframe has microphone permission via the allow attribute.
475- */
476- const hasIframeMicrophonePermission = async ( ) : Promise < boolean > => {
477- if ( typeof window === 'undefined' || typeof document === 'undefined' ) {
478- return false ;
479- }
480-
481- try {
482- // Try using the Permissions Policy API (Chrome 88+, Edge 88+)
483- const doc = document as any ;
484- if ( doc . permissionsPolicy && typeof doc . permissionsPolicy . allowsFeature === 'function' ) {
485- return doc . permissionsPolicy . allowsFeature ( 'microphone' ) ;
486- }
487-
488- // Fallback to deprecated Feature Policy API (Chrome 60-87, Edge 79-87)
489- if ( doc . featurePolicy && typeof doc . featurePolicy . allowsFeature === 'function' ) {
490- return doc . featurePolicy . allowsFeature ( 'microphone' ) ;
491- }
492-
493- // Fallback to Permissions API (broader support: Chrome 43+, Firefox 46+, Safari 16+)
494- if ( typeof navigator !== 'undefined' && navigator . permissions ) {
495- const result = await navigator . permissions . query ( { name : 'microphone' as PermissionName } ) ;
496- // 'granted' or 'prompt' means microphone is allowed by iframe policy
497- // 'denied' means either user denied or iframe policy blocks it
498- return result . state !== 'denied' ;
499- }
500- } catch ( e ) {
501- // If permissions check fails, assume microphone is not allowed in iframe
502- }
503-
504- return false ;
505- }
506-
507463export interface IBotConnection {
508464 connectionStatus$ : BehaviorSubject < ConnectionStatus > ,
509465 activity$ : Observable < Activity > ,
@@ -1281,19 +1237,26 @@ export class DirectLine implements IBotConnection {
12811237 }
12821238
12831239 /**
1284- * Modifies stream URL for voice mode: replaces /stream with /stream/multimodal
1240+ * Modifies stream URL for voice mode: appends /multimodal to the /stream path
1241+ * while preserving query string, hash, and other URL parts.
12851242 */
12861243 private getMultimodalStreamUrl ( url : string ) : string {
12871244 if ( ! this . voiceModeEnabled || ! url ) {
12881245 return url ;
12891246 }
12901247
1291- // Replace /stream endpoint with /stream/multimodal (if not already multimodal)
1292- if ( ! url . includes ( '/stream/multimodal' ) ) {
1293- return url . replace ( '/stream' , '/stream/multimodal' ) ;
1294- }
1248+ try {
1249+ const parsed = new URL ( url ) ;
1250+
1251+ if ( parsed . pathname . endsWith ( '/stream' ) ) {
1252+ parsed . pathname += '/multimodal' ;
1253+ }
12951254
1296- return url ;
1255+ return parsed . toString ( ) ;
1256+ } catch {
1257+ // If URL parsing fails (malformed URL), return as-is
1258+ return url ;
1259+ }
12971260 }
12981261
12991262}
0 commit comments