PageSourceSearch

https://bambuser.com/docs/assets/js/7e65b7fc.84f908b1.js

js bambuser.com collected 2026-09-24 08:28:54 UTC 12,188 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunkbambuser_docs||=[]).push([[2788],{12583(e,i,n){n.r(i),n.d(i,{assets:()=>l,contentTitle:()=>a,default:()=>h,frontMatter:()=>r,metadata:()=>t,toc:()=>c});const t=JSON.parse('{"id":"mobileSDK/ios/vc-ios-picture-in-picture","title":"iOS SDK \u2014 Picture-in-Picture","description":"Configure the floating mini-player, enter and exit PiP programmatically, control PiP frame size, and observe presentation changes from the widget.","source":"@site/video-consultation/mobileSDK/ios/picture-in-picture.mdx","sourceDirName":"mobileSDK/ios","slug":"/mobileSDK/ios/mobile-sdk/ios/picture-in-picture","permalink":"/docs/video-consultation/mobileSDK/ios/mobile-sdk/ios/picture-in-picture","draft":false,"unlisted":false,"tags":[],"version":"current","frontMatter":{"id":"vc-ios-picture-in-picture","title":"iOS SDK \u2014 Picture-in-Picture","description":"Configure the floating mini-player, enter and exit PiP programmatically, control PiP frame size, and observe presentation changes from the widget.","sidebar_label":"Picture in Picture","slug":"mobile-sdk/ios/picture-in-picture"},"sidebar":"someSidebars","previous":{"title":"Shopper Events Tracking","permalink":"/docs/video-consultation/mobileSDK/ios/mobile-sdk/ios/purchase-tracking"},"next":{"title":"Co-browse","permalink":"/docs/video-consultation/mobileSDK/ios/mobile-sdk/ios/co-browse"}}');var o=n(74848),s=n(28453);const r={id:"vc-ios-picture-in-picture",title:"iOS SDK \u2014 Picture-in-Picture",description:"Configure the floating mini-player, enter and exit PiP programmatically, control PiP frame size, and observe presentation changes from the widget.",sidebar_label:"Picture in Picture",slug:"mobile-sdk/ios/picture-in-picture"},a="Picture in Picture",l={},c=[{value:"Two PiP modes",id:"two-pip-modes",level:2},{value:"Entering and exiting PiP",id:"entering-and-exiting-pip",level:2},{value:"Entering PiP on checkout",id:"entering-pip-on-checkout",level:2},{value:"Auto-expand on connect",id:"auto-expand-on-connect",level:2},{value:"Frame size and position",id:"frame-size-and-position",level:2},{value:"Observing presentation changes",id:"observing-presentation-changes",level:2},{value:"Presentation transitions",id:"presentation-transitions",level:2},{value:"Related \u2014 audio during PiP",id:"related--audio-during-pip",level:2},{value:"See also",id:"see-also",level:2}];function d(e){const i={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(i.header,{children:(0,o.jsx)(i.h1,{id:"picture-in-picture",children:"Picture in Picture"})}),"\n",(0,o.jsxs)(i.p,{children:["The Calls-shopper SDK's Picture-in-Picture is a ",(0,o.jsx)(i.strong,{children:"draggable floating mini-player"})," rendered by the SDK itself \u2014 not iOS's system-level ",(0,o.jsx)(i.code,{children:"AVPictureInPictureController"}),". The mini-player is a smaller frame of the same ",(0,o.jsx)(i.code,{children:"WKWebView"})," the full-screen widget uses, so the video stream keeps flowing and every widget control still works, just at a smaller footprint."]}),"\n",(0,o.jsx)(i.h2,{id:"two-pip-modes",children:"Two PiP modes"}),"\n",(0,o.jsxs)(i.p,{children:[(0,o.jsx)(i.code,{children:"BambuserCallController"})," exposes:"]}),"\n",(0,o.jsxs)(i.ul,{children:["\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.code,{children:"isPiP: Bool"})," \u2014 is the overlay currently in mini-player mode?"]}),"\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.code,{children:"pipPresentation: PipPresentation"})," \u2014 ",(0,o.jsx)(i.code,{children:".floating"})," or ",(0,o.jsx)(i.code,{children:".minimized"}),"."]}),"\n"]}),"\n",(0,o.jsxs)(i.p,{children:[(0,o.jsx)(i.code,{children:"PipPresentation.floating"})," is the standard mini-player (default ",(0,o.jsx)(i.code,{children:"180\xd7260 pt"}),"). ",(0,o.jsx)(i.code,{children:".minimized"})," is a more compact chip variant (default ",(0,o.jsx)(i.code,{children:"180\xd760 pt"}),") used when the widget wants an even smaller footprint (e.g. during a queue wait)."]}),"\n",(0,o.jsx)(i.h2,{id:"entering-and-exiting-pip",children:"Entering and exiting PiP"}),"\n",(0,o.jsxs)(i.p,{children:["The shopper enters PiP by tapping the widget's own minimize button. That triggers a ",(0,o.jsx)(i.co
1de,{children:"viddget:viewport_mode"})," message which the SDK's event router translates to ",(0,o.jsx)(i.code,{children:"controller.isPiP = true"}),". Widget-initiated entries stay widget-controlled \u2014 no host code needed."]}),"\n",(0,o.jsx)(i.p,{children:"From the host side you can drive it programmatically:"}),"\n",(0,o.jsx)(i.pre,{children:(0,o.jsx)(i.code,{className:"language-swift",children:"bambuserCall.enterPiP()                          // \u2192 floating\nbambuserCall.enterPiP(presentation: .minimized)  // \u2192 minimized chip\nbambuserCall.expand()                            // \u2192 back to full-screen\n"})}),"\n",(0,o.jsxs)(i.p,{children:[(0,o.jsx)(i.code,{children:"enterPiP(presentation:)"})," drives ",(0,o.jsx)(i.strong,{children:"both sides"}),":"]}),"\n",(0,o.jsxs)(i.ol,{children:["\n",(0,o.jsxs)(i.li,{children:["Tells the widget to switch its own UI to the mini video player (via the embed's ",(0,o.jsx)(i.code,{children:"floatAbove"})," method). Otherwise the widget would keep rendering whatever it was showing \u2014 chat, cart, product detail \u2014 inside the shrunken frame."]}),"\n",(0,o.jsx)(i.li,{children:"Collapses the SDK's WebView layout to the floating / minimized frame."}),"\n"]}),"\n",(0,o.jsx)(i.h2,{id:"entering-pip-on-checkout",children:"Entering PiP on checkout"}),"\n",(0,o.jsxs)(i.p,{children:["The most common host-side use of ",(0,o.jsx)(i.code,{children:"enterPiP"})," is on the widget's checkout event. When the agent adds items to the shopper's cart and the shopper taps \"Checkout\" inside the widget, the SDK emits a ",(0,o.jsx)(i.code,{children:".checkout"})," event. Host apps typically respond by shrinking to a floating mini-player and switching the underlying UI to the app's own checkout screen, so the shopper can complete the purchase while the call continues in the corner:"]}),"\n",(0,o.jsx)(i.pre,{children:(0,o.jsx)(i.code,{className:"language-swift",children:"func bambuserCall(_ controller: BambuserCallController, didEmit event: BambuserCallEvent) {\n    switch event {\n    case .checkout:\n        // Shrink to a floating mini video player so the shopper can\n        // complete checkout in the app underneath.\n        controller.enterPiP(presentation: .floating)\n        navigation.switchTo(.cart)   // your app's own checkout route\n    // \u2026\n    }\n}\n"})}),"\n",(0,o.jsx)(i.p,{children:"Without this, the widget's checkout tap would leave the video full-screen and the shopper would have no way to reach the app's native cart / payment UI."}),"\n",(0,o.jsx)(i.h2,{id:"auto-expand-on-connect",children:"Auto-expand on connect"}),"\n",(0,o.jsxs)(i.p,{children:["By default, when the call transitions to ",(0,o.jsx)(i.code,{children:"connected"})," while in PiP, the SDK auto-restores the widget to full-screen so the shopper sees the agent's face immediately. Configure via:"]}),"\n",(0,o.jsx)(i.pre,{children:(0,o.jsx)(i.code,{className:"language-swift",children:'BambuserCallConfiguration(\n    orgId: "\u2026",\n    environment: .us,\n    autoExpandOnConnect: true    // default\n)\n'})}),"\n",(0,o.jsxs)(i.p,{children:["Set ",(0,o.jsx)(i.code,{children:"false"})," if you want the shopper to stay in PiP through the connection and expand manually."]}),"\n",(0,o.jsx)(i.h2,{id:"frame-size-and-position",children:"Frame size and position"}),"\n",(0,o.jsxs)(i.p,{children:["The mini-player defaults to ",(0,o.jsx)(i.code,{children:"180\xd7260 pt"})," (",(0,o.jsx)(i.code,{children:".floating"}),") and ",(0,o.jsx)(i.code,{children:"180\xd760 pt"})," (",(0,o.jsx)(i.code,{children:".minimized"}),"). Both are configurable at init time:"]}),"\n",(0,o.jsx)(i.pre,{children:(0,o.jsx)(i.code,{className:"language-swift",children:'BambuserCallConfiguration(\n    orgId: "\u2026",\n    environment: .us,\n    floatingPipSize: CGSize(width: 200, height: 300),\n    minimizedPipSize: CGSize(width: 200, height: 72)\n)\n'})}),"\n",(0,o.jsx)(i.p,{children:"The mini-player is anchored bottom-right by default. The shopper can drag it anywhere on screen \u2014 the drag gesture only fires in PiP mode (never in full-screen, so it never competes with WebKit's own gesture recognizers)."}),"\n",(0,o.jsx)(i.p,{children:"Positioning constants (margin from screen edges, bottom safe-area reserve) can be tuned inside the configuration:"}),"\n",(0,o.jsxs)(i.ul,{children:["\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.co
1de,{children:"pipMargin"})," \u2014 CGFloat, distance from screen edges (default ",(0,o.jsx)(i.code,{children:"16"}),")."]}),"\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.code,{children:"bottomReserve"})," \u2014 CGFloat, additional bottom offset for tab bars (default ",(0,o.jsx)(i.code,{children:"0"}),"; set to your tab-bar height if you want the PiP to float above it)."]}),"\n"]}),"\n",(0,o.jsx)(i.h2,{id:"observing-presentation-changes",children:"Observing presentation changes"}),"\n",(0,o.jsxs)(i.p,{children:["The ",(0,o.jsx)(i.code,{children:"BambuserCallDelegate"})," protocol receives ",(0,o.jsx)(i.code,{children:".presentationChanged"}),":"]}),"\n",(0,o.jsx)(i.pre,{children:(0,o.jsx)(i.code,{className:"language-swift",children:'func bambuserCall(_ controller: BambuserCallController, didEmit event: BambuserCallEvent) {\n    if case .presentationChanged(let isPiP, let presentation) = event {\n        print("PiP=\\(isPiP), presentation=\\(presentation)")\n    }\n}\n'})}),"\n",(0,o.jsx)(i.p,{children:"SwiftUI hosts can observe directly on the controller:"}),"\n",(0,o.jsx)(i.pre,{children:(0,o.jsx)(i.code,{className:"language-swift",children:"struct MyView: View {\n    @ObservedObject var call: BambuserCallController\n\n    var body: some View {\n        // \u2026\n            .onChange(of: call.isPiP) { newValue in\n                // react to PiP transitions\n            }\n    }\n}\n"})}),"\n",(0,o.jsx)(i.h2,{id:"presentation-transitions",children:"Presentation transitions"}),"\n",(0,o.jsxs)(i.p,{children:["The full-screen \u2194 PiP animation is a UIKit spring (",(0,o.jsx)(i.code,{children:"duration = 0.35, damping = 0.85"}),"), applied via ",(0,o.jsx)(i.code,{children:"UIView.animate"})," on the WebView's layout constraints. Both mount surfaces (SwiftUI and UIKit) share this \u2014 you won't see two different animations in a mixed-host app."]}),"\n",(0,o.jsx)(i.h2,{id:"related--audio-during-pip",children:"Related \u2014 audio during PiP"}),"\n",(0,o.jsxs)(i.p,{children:["Add ",(0,o.jsx)(i.code,{children:"UIBackgroundModes: audio"})," to Info.plist so the call audio stays alive during brief backgrounding (Control Center, incoming banner notifications). Without it, WebRTC audio can suspend and the video stream will stall momentarily. See ",(0,o.jsx)(i.a,{href:"/docs/video-consultation/mobileSDK/ios/mobile-sdk/ios/installation",children:"Installation"})," for the full Info.plist snippet."]}),"\n",(0,o.jsx)(i.h2,{id:"see-also",children:"See also"}),"\n",(0,o.jsxs)(i.ul,{children:["\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.a,{href:"/docs/video-consultation/mobileSDK/ios/mobile-sdk/ios/co-browse",children:"Co-browse"})," \u2014 how PiP interacts with in-app navigation during a call."]}),"\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.a,{href:"/docs/video-consultation/mobileSDK/ios/mobile-sdk/ios/architecture",children:"Architecture"})," \u2014 the presentation stack in detail."]}),"\n"]})]})}function h(e={}){const{wrapper:i}={...(0,s.R)(),...e.components};return i?(0,o.jsx)(i,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},28453(e,i,n){n.d(i,{R:()=>r,x:()=>a});var t=n(96540);const o={},s=t.createContext(o);function r(e){const i=t.useContext(s);return t.useMemo(function(){return"function"==typeof e?e(i):{...i,...e}},[i,e])}function a(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),t.createElement(s.Provider,{value:i},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.