Add PictureInPicture feature
This commit is contained in:
parent
918c38d718
commit
1b28af9131
|
@ -247,57 +247,6 @@
|
|||
</objects>
|
||||
<point key="canvasLocation" x="749" y="402"/>
|
||||
</scene>
|
||||
<!--Playback-->
|
||||
<scene sceneID="sgb-vK-JUQ">
|
||||
<objects>
|
||||
<viewController id="M6P-U9-OQB" userLabel="Playback" customClass="PlaybackController" customModule="Example_iOS" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="1Nb-68-FWe"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xon-CD-cIo"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Z5m-1T-AfH">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="eXp-4i-FnG" customClass="GLHKView" customModule="HaishinKit">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
</view>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="lKf-53-lJb">
|
||||
<rect key="frame" x="348" y="767" width="30" height="30"/>
|
||||
<color key="backgroundColor" systemColor="systemBlueColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="30" id="Abw-7u-gO4"/>
|
||||
<constraint firstAttribute="height" constant="30" id="dxX-97-jzH"/>
|
||||
</constraints>
|
||||
<state key="normal" title="▶︎">
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="didPlaybackButtonTap:" destination="M6P-U9-OQB" eventType="touchUpInside" id="PKd-Aq-bAF"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="eXp-4i-FnG" secondAttribute="trailing" id="2Eu-cX-ofs"/>
|
||||
<constraint firstItem="eXp-4i-FnG" firstAttribute="top" secondItem="Z5m-1T-AfH" secondAttribute="top" id="4kR-wF-kbE"/>
|
||||
<constraint firstItem="xon-CD-cIo" firstAttribute="top" secondItem="lKf-53-lJb" secondAttribute="bottom" constant="16" id="CaM-6n-hrp"/>
|
||||
<constraint firstAttribute="bottom" secondItem="eXp-4i-FnG" secondAttribute="bottom" id="Hf1-6l-t8q"/>
|
||||
<constraint firstItem="eXp-4i-FnG" firstAttribute="leading" secondItem="Z5m-1T-AfH" secondAttribute="leading" id="bbC-24-GqG"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="lKf-53-lJb" secondAttribute="trailing" constant="16" id="o1U-y6-vLF"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<tabBarItem key="tabBarItem" title="Playback" id="cnH-RJ-T8G"/>
|
||||
<navigationItem key="navigationItem" id="DHF-rz-qM4"/>
|
||||
<connections>
|
||||
<outlet property="lfView" destination="eXp-4i-FnG" id="0p8-El-ZPC"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="uoF-Er-2sg" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1602.8985507246377" y="0.0"/>
|
||||
</scene>
|
||||
<!--Tab Bar Controller-->
|
||||
<scene sceneID="yl2-sM-qoP">
|
||||
<objects>
|
||||
|
@ -310,14 +259,87 @@
|
|||
</tabBar>
|
||||
<connections>
|
||||
<segue destination="9pv-A4-QxB" kind="relationship" relationship="viewControllers" id="u7Y-xg-7CH"/>
|
||||
<segue destination="M6P-U9-OQB" kind="relationship" relationship="viewControllers" id="pqx-AR-kNX"/>
|
||||
<segue destination="8rJ-Kc-sve" kind="relationship" relationship="viewControllers" id="lzU-1b-eKA"/>
|
||||
<segue destination="uu9-Wt-Dxt" kind="relationship" relationship="viewControllers" id="KIw-Gf-bHs"/>
|
||||
<segue destination="8rJ-Kc-sve" kind="relationship" relationship="viewControllers" id="V0f-g5-FP2"/>
|
||||
</connections>
|
||||
</tabBarController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="HuB-VB-40B" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="0.0" y="0.0"/>
|
||||
</scene>
|
||||
<!--Playback-->
|
||||
<scene sceneID="2gE-y3-VQT">
|
||||
<objects>
|
||||
<viewController id="uu9-Wt-Dxt" customClass="PlaybackContainerViewController" customModule="Example_iOS" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="O2h-r1-E7F"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="dXy-vE-8ph"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="7NS-AC-cs3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Awa-Nz-XRH">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="813"/>
|
||||
<connections>
|
||||
<segue destination="S4s-US-fOh" kind="embed" id="ahj-EG-JS5"/>
|
||||
</connections>
|
||||
</containerView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Awa-Nz-XRH" firstAttribute="leading" secondItem="7NS-AC-cs3" secondAttribute="leading" id="3qS-OU-Vri"/>
|
||||
<constraint firstItem="dXy-vE-8ph" firstAttribute="top" secondItem="Awa-Nz-XRH" secondAttribute="bottom" id="6Zm-KN-92x"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Awa-Nz-XRH" secondAttribute="trailing" id="lWs-68-EwZ"/>
|
||||
<constraint firstItem="Awa-Nz-XRH" firstAttribute="top" secondItem="7NS-AC-cs3" secondAttribute="top" id="yAY-Zt-w6U"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<tabBarItem key="tabBarItem" title="Playback" id="Pv0-9w-P0v"/>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="O4Q-V8-kNz" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1463.768115942029" y="-102.45535714285714"/>
|
||||
</scene>
|
||||
<!--Playback View Controller-->
|
||||
<scene sceneID="t76-yU-vlu">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="PlaybackViewController" id="S4s-US-fOh" customClass="PlaybackViewController" customModule="Example_iOS" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="vko-dD-pI7"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="4an-yi-rp6"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="mU9-Qc-s55" customClass="GLHKView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="813"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="p4J-1x-O1c">
|
||||
<rect key="frame" x="368" y="32" width="30" height="30"/>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="1" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="30" id="9tM-9Y-bwe"/>
|
||||
<constraint firstAttribute="width" constant="30" id="Xcd-dW-hq8"/>
|
||||
</constraints>
|
||||
<color key="tintColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
<state key="normal" title="●"/>
|
||||
<connections>
|
||||
<action selector="didPlaybackButtonTap:" destination="S4s-US-fOh" eventType="touchDown" id="bAJ-ml-suS"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemGray2Color" red="0.68235294120000001" green="0.68235294120000001" blue="0.69803921570000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="p4J-1x-O1c" secondAttribute="trailing" constant="16" id="f5f-rE-eQ2"/>
|
||||
<constraint firstItem="p4J-1x-O1c" firstAttribute="top" secondItem="mU9-Qc-s55" secondAttribute="top" constant="32" id="tXc-VA-Erm"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="playbackButton" destination="p4J-1x-O1c" id="ZN3-UM-SXt"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="A6d-pe-nwN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2110" y="-102"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="first" width="30" height="30"/>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
|
||||
final class PlaybackContainerViewController: UIViewController {
|
||||
}
|
|
@ -2,10 +2,11 @@ import Foundation
|
|||
import HaishinKit
|
||||
import UIKit
|
||||
|
||||
final class PlaybackController: UIViewController {
|
||||
final class PlaybackViewController: UIViewController, HKPictureInPicureController {
|
||||
private static let maxRetryCount: Int = 5
|
||||
|
||||
@IBOutlet private weak var lfView: GLHKView?
|
||||
@IBOutlet private weak var playbackButton: UIButton!
|
||||
|
||||
private var rtmpConnection = RTMPConnection()
|
||||
private var rtmpStream: RTMPStream!
|
||||
|
@ -14,21 +15,20 @@ final class PlaybackController: UIViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
rtmpStream = RTMPStream(connection: rtmpConnection)
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapped(_:)))
|
||||
tapGesture.numberOfTapsRequired = 2
|
||||
view.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
logger.info("viewWillAppear")
|
||||
super.viewWillAppear(animated)
|
||||
rtmpStream.addObserver(self, forKeyPath: "currentFPS", options: .new, context: nil)
|
||||
lfView?.attachStream(rtmpStream)
|
||||
(view as? GLHKView)?.attachStream(rtmpStream)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
logger.info("viewWillDisappear")
|
||||
super.viewWillDisappear(animated)
|
||||
rtmpStream.removeObserver(self, forKeyPath: "currentFPS")
|
||||
rtmpStream.close()
|
||||
rtmpStream.dispose()
|
||||
}
|
||||
|
||||
@IBAction func didPlaybackButtonTap(_ button: UIButton) {
|
||||
|
@ -60,7 +60,7 @@ final class PlaybackController: UIViewController {
|
|||
retryCount = 0
|
||||
rtmpStream.play(Preference.defaultInstance.streamName!)
|
||||
case RTMPConnection.Code.connectFailed.rawValue, RTMPConnection.Code.connectClosed.rawValue:
|
||||
guard retryCount <= PlaybackController.maxRetryCount else {
|
||||
guard retryCount <= PlaybackViewController.maxRetryCount else {
|
||||
return
|
||||
}
|
||||
Thread.sleep(forTimeInterval: pow(2.0, Double(retryCount)))
|
||||
|
@ -71,6 +71,17 @@ final class PlaybackController: UIViewController {
|
|||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func didTapped(_ sender: UITapGestureRecognizer) {
|
||||
if isPictureInPictureActive {
|
||||
stopPictureInPicture()
|
||||
playbackButton.isHidden = false
|
||||
} else {
|
||||
startPictureInPicture()
|
||||
playbackButton.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func rtmpErrorHandler(_ notification: Notification) {
|
||||
logger.error(notification)
|
||||
|
@ -86,10 +97,4 @@ final class PlaybackController: UIViewController {
|
|||
private func didBecomeActive(_ notification: Notification) {
|
||||
rtmpStream.receiveVideo = true
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
|
||||
if Thread.isMainThread {
|
||||
// currentFPSLabel?.text = "\(rtmpStream.currentFPS)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -373,6 +373,9 @@
|
|||
BC9CFA9423BDE8B700917EEF /* NetStreamRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC9CFA9223BDE8B700917EEF /* NetStreamRenderer.swift */; };
|
||||
BC9CFA9523BDE8B700917EEF /* NetStreamRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC9CFA9223BDE8B700917EEF /* NetStreamRenderer.swift */; };
|
||||
BCFB355524FA27EA00DC5108 /* PlaybackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFB355324FA275600DC5108 /* PlaybackViewController.swift */; };
|
||||
BCFB355A24FA40DD00DC5108 /* PlaybackContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFB355924FA40DD00DC5108 /* PlaybackContainerViewController.swift */; };
|
||||
BCFB355C24FAB29B00DC5108 /* HKPictureInPicureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFB355624FA37F700DC5108 /* HKPictureInPicureController.swift */; };
|
||||
BCFB355E24FAB2D200DC5108 /* HKPictureInPicureControllerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFB355D24FAB2D200DC5108 /* HKPictureInPicureControllerImpl.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -667,6 +670,9 @@
|
|||
BC83A4722403D83B006BDE06 /* VTCompressionSession+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VTCompressionSession+Extension.swift"; sourceTree = "<group>"; };
|
||||
BC9CFA9223BDE8B700917EEF /* NetStreamRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetStreamRenderer.swift; sourceTree = "<group>"; };
|
||||
BCFB355324FA275600DC5108 /* PlaybackViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackViewController.swift; sourceTree = "<group>"; };
|
||||
BCFB355624FA37F700DC5108 /* HKPictureInPicureController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKPictureInPicureController.swift; sourceTree = "<group>"; };
|
||||
BCFB355924FA40DD00DC5108 /* PlaybackContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackContainerViewController.swift; sourceTree = "<group>"; };
|
||||
BCFB355D24FAB2D200DC5108 /* HKPictureInPicureControllerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKPictureInPicureControllerImpl.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -1007,6 +1013,7 @@
|
|||
29205CBD1E461F4E009D3FFF /* Main.storyboard */,
|
||||
296897411CDB01D20074D5F0 /* AppDelegate.swift */,
|
||||
296897441CDB01D20074D5F0 /* LiveViewController.swift */,
|
||||
BCFB355924FA40DD00DC5108 /* PlaybackContainerViewController.swift */,
|
||||
BCFB355324FA275600DC5108 /* PlaybackViewController.swift */,
|
||||
291468161E581C7D00E619BA /* Preference.swift */,
|
||||
2950742E1E4620B7007F15A4 /* PreferenceViewController.swift */,
|
||||
|
@ -1064,6 +1071,8 @@
|
|||
29D3D4D41ED04E7100DD4AA6 /* DeviceUtil+Extenstion.swift */,
|
||||
29C263171D00804A0098D4EF /* GLHKView.swift */,
|
||||
299F7E3B1CD71A97001E7272 /* HaishinKit.h */,
|
||||
BCFB355624FA37F700DC5108 /* HKPictureInPicureController.swift */,
|
||||
BCFB355D24FAB2D200DC5108 /* HKPictureInPicureControllerImpl.swift */,
|
||||
299B13261D3B751400A1E8F5 /* HKView.swift */,
|
||||
299F7E3A1CD71A97001E7272 /* Info.plist */,
|
||||
2999C3742071138F00892E55 /* MTHKView.swift */,
|
||||
|
@ -1636,6 +1645,7 @@
|
|||
2943ED53232FCA7C00ED6301 /* Setting.swift in Sources */,
|
||||
BC3FA38C2413AEDA009C83D3 /* AVFoundation+Extension.swift in Sources */,
|
||||
2915EC4D1D85BB8C00621092 /* RTMPTSocket.swift in Sources */,
|
||||
BCFB355C24FAB29B00DC5108 /* HKPictureInPicureController.swift in Sources */,
|
||||
2958910A1EEB8D1800CE51E1 /* FLVReader.swift in Sources */,
|
||||
29C2631C1D0083B50098D4EF /* VideoIOComponent.swift in Sources */,
|
||||
29B876B41CD70B2800FC07DA /* RTMPSharedObject.swift in Sources */,
|
||||
|
@ -1705,6 +1715,7 @@
|
|||
29B876781CD70ACE00FC07DA /* HTTPService.swift in Sources */,
|
||||
2976A4861D4903C300B53EF2 /* DeviceUtil.swift in Sources */,
|
||||
29C263181D00804A0098D4EF /* GLHKView.swift in Sources */,
|
||||
BCFB355E24FAB2D200DC5108 /* HKPictureInPicureControllerImpl.swift in Sources */,
|
||||
29B876881CD70AE800FC07DA /* TransportStream.swift in Sources */,
|
||||
29B876BE1CD70B3900FC07DA /* EventDispatcher.swift in Sources */,
|
||||
29B8769D1CD70B1100FC07DA /* NetService.swift in Sources */,
|
||||
|
@ -1874,6 +1885,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
291468191E581C8F00E619BA /* Preference.swift in Sources */,
|
||||
BCFB355A24FA40DD00DC5108 /* PlaybackContainerViewController.swift in Sources */,
|
||||
296897671CDB02940074D5F0 /* AppDelegate.swift in Sources */,
|
||||
296897681CDB02940074D5F0 /* LiveViewController.swift in Sources */,
|
||||
BCFB355524FA27EA00DC5108 /* PlaybackViewController.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
|
||||
private var HKPictureInPicureControllerImplKey: UInt8 = 0
|
||||
|
||||
public enum HKPictureInPicureControllerPosition {
|
||||
case topLeft
|
||||
case topRight
|
||||
case bottomRight
|
||||
case bottomLeft
|
||||
|
||||
func CGPoint(_ controller: HKPictureInPicureController, insets: UIEdgeInsets = .zero) -> CGPoint {
|
||||
let margin = controller.pictureInPictureMargin
|
||||
switch self {
|
||||
case .topLeft:
|
||||
return .init(
|
||||
x: margin + insets.left,
|
||||
y: margin + insets.top)
|
||||
case .topRight:
|
||||
return .init(
|
||||
x: UIScreen.main.bounds.width - controller.pictureInPictureSize.width - margin - insets.right,
|
||||
y: margin + insets.top
|
||||
)
|
||||
case .bottomLeft:
|
||||
return .init(
|
||||
x: margin + insets.left,
|
||||
y: UIScreen.main.bounds.height - controller.pictureInPictureSize.height - margin - insets.bottom
|
||||
)
|
||||
case .bottomRight:
|
||||
return .init(
|
||||
x: UIScreen.main.bounds.width - controller.pictureInPictureSize.width - margin - insets.right,
|
||||
y: UIScreen.main.bounds.height - controller.pictureInPictureSize.height - margin - insets.bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public protocol HKPictureInPicureController: class {
|
||||
var isPictureInPictureActive: Bool { get }
|
||||
var pictureInPictureSize: CGSize { get set }
|
||||
var pictureInPicturePosition: HKPictureInPicureControllerPosition { get set }
|
||||
var pictureInPictureMargin: CGFloat { get set }
|
||||
var pictureInPictureCornerRadius: CGFloat { get set }
|
||||
var pictureInPictureAnimationDuration: TimeInterval { get set }
|
||||
|
||||
func startPictureInPicture()
|
||||
func stopPictureInPicture()
|
||||
}
|
||||
|
||||
public extension HKPictureInPicureController where Self: UIViewController {
|
||||
var isPictureInPictureActive: Bool {
|
||||
impl.isPictureInPictureActive
|
||||
}
|
||||
|
||||
var pictureInPictureSize: CGSize {
|
||||
get {
|
||||
impl.pictureInPictureSize
|
||||
}
|
||||
set {
|
||||
impl.pictureInPictureSize = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var pictureInPicturePosition: HKPictureInPicureControllerPosition {
|
||||
get {
|
||||
impl.pictureInPicturePosition
|
||||
}
|
||||
set {
|
||||
impl.pictureInPicturePosition = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var pictureInPictureMargin: CGFloat {
|
||||
get {
|
||||
impl.pictureInPictureMargin
|
||||
}
|
||||
set {
|
||||
impl.pictureInPictureMargin = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var pictureInPictureCornerRadius: CGFloat {
|
||||
get {
|
||||
impl.pictureInPictureCornerRadius
|
||||
}
|
||||
set {
|
||||
impl.pictureInPictureCornerRadius = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var pictureInPictureAnimationDuration: TimeInterval {
|
||||
get {
|
||||
impl.pictureInPictureAnimationDuration
|
||||
}
|
||||
set {
|
||||
impl.pictureInPictureAnimationDuration = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var impl: HKPictureInPicureControllerImpl {
|
||||
get {
|
||||
guard let object = objc_getAssociatedObject(self, &HKPictureInPicureControllerImplKey) as? HKPictureInPicureControllerImpl else {
|
||||
let impl = HKPictureInPicureControllerImpl(self)
|
||||
objc_setAssociatedObject(self, &HKPictureInPicureControllerImplKey, impl, .OBJC_ASSOCIATION_RETAIN)
|
||||
return impl
|
||||
}
|
||||
return object
|
||||
}
|
||||
set {
|
||||
objc_setAssociatedObject(self, &HKPictureInPicureControllerImplKey, newValue, .OBJC_ASSOCIATION_RETAIN)
|
||||
}
|
||||
}
|
||||
|
||||
func startPictureInPicture() {
|
||||
guard !impl.isPictureInPictureActive else {
|
||||
return
|
||||
}
|
||||
impl.startPictureInPicture()
|
||||
}
|
||||
|
||||
func stopPictureInPicture() {
|
||||
guard impl.isPictureInPictureActive else {
|
||||
return
|
||||
}
|
||||
impl.stopPictureInPicture()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
import Foundation
|
||||
|
||||
class HKPictureInPicureControllerImpl: HKPictureInPicureController {
|
||||
static let margin: CGFloat = 16
|
||||
static let position: HKPictureInPicureControllerPosition = .bottomRight
|
||||
static let cornerRadius: CGFloat = 8
|
||||
static let animationDuration: TimeInterval = 0.3
|
||||
|
||||
// MARK: HKPictureInPicureController
|
||||
var isPictureInPictureActive: Bool = false
|
||||
var pictureInPictureSize: CGSize = .init(width: 160, height: 90)
|
||||
var pictureInPictureMargin: CGFloat = HKPictureInPicureControllerImpl.margin
|
||||
var pictureInPicturePosition: HKPictureInPicureControllerPosition = HKPictureInPicureControllerImpl.position
|
||||
var pictureInPictureCornerRadius: CGFloat = HKPictureInPicureControllerImpl.cornerRadius
|
||||
var pictureInPictureAnimationDuration: TimeInterval = HKPictureInPicureControllerImpl.animationDuration
|
||||
|
||||
private var window: UIWindow?
|
||||
private var origin: CGPoint = .zero
|
||||
private var parent: UIViewController?
|
||||
private let viewController: UIViewController
|
||||
private lazy var panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didPanGestureRecognizer(_:)))
|
||||
|
||||
init(_ viewController: UIViewController) {
|
||||
self.viewController = viewController
|
||||
self.parent = viewController.parent
|
||||
}
|
||||
|
||||
func startPictureInPicture() {
|
||||
guard !isPictureInPictureActive else {
|
||||
return
|
||||
}
|
||||
toggleWindow()
|
||||
viewController.view.addGestureRecognizer(panGestureRecognizer)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(orientationDidChange), name: UIDevice.orientationDidChangeNotification, object: nil)
|
||||
isPictureInPictureActive = true
|
||||
}
|
||||
|
||||
func stopPictureInPicture() {
|
||||
guard isPictureInPictureActive else {
|
||||
return
|
||||
}
|
||||
toggleWindow()
|
||||
viewController.view.removeGestureRecognizer(panGestureRecognizer)
|
||||
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
|
||||
isPictureInPictureActive = false
|
||||
}
|
||||
|
||||
private func toggleWindow() {
|
||||
if viewController.view.window?.rootViewController == viewController {
|
||||
if window == nil {
|
||||
window = viewController.view.window
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
transform(parent?.view.window?.safeAreaInsets ?? .zero)
|
||||
} else {
|
||||
transform()
|
||||
}
|
||||
viewController.view.layer.cornerRadius = pictureInPictureCornerRadius
|
||||
} else {
|
||||
viewController.view.layer.cornerRadius = 0
|
||||
UIView.animate(withDuration: pictureInPictureAnimationDuration) { [weak self] in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
self.window?.frame = UIScreen.main.bounds
|
||||
}
|
||||
window = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
if window == nil {
|
||||
viewController.removeFromParent()
|
||||
viewController.view.removeFromSuperview()
|
||||
window = UIWindow(frame: .zero)
|
||||
window?.rootViewController = viewController
|
||||
window?.makeKeyAndVisible()
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
transform(parent?.view.window?.safeAreaInsets ?? .zero)
|
||||
} else {
|
||||
transform()
|
||||
}
|
||||
viewController.view.layer.cornerRadius = pictureInPictureCornerRadius
|
||||
} else {
|
||||
window?.rootViewController = nil
|
||||
window = nil
|
||||
parent?.addChild(viewController)
|
||||
parent?.view.addSubview(viewController.view)
|
||||
viewController.view.layer.cornerRadius = 0
|
||||
UIView.animate(withDuration: pictureInPictureAnimationDuration) { [weak self] in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
self.viewController.view.frame = self.parent?.view.bounds ?? .zero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func transform(_ insets: UIEdgeInsets = .zero) {
|
||||
UIView.animate(withDuration: pictureInPictureAnimationDuration) { [weak self] in
|
||||
guard let self = self, let window = self.window else {
|
||||
return
|
||||
}
|
||||
if self.origin == .zero || !UIScreen.main.bounds.contains(self.origin) {
|
||||
window.frame = .init(origin: self.pictureInPicturePosition.CGPoint(self, insets: insets), size: self.pictureInPictureSize)
|
||||
} else {
|
||||
window.frame = .init(origin: self.origin, size: self.pictureInPictureSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func orientationDidChange() {
|
||||
switch UIDevice.current.orientation {
|
||||
case .landscapeLeft, .landscapeRight, .portrait, .portraitUpsideDown:
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
transform(parent?.view.window?.safeAreaInsets ?? .zero)
|
||||
} else {
|
||||
transform()
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func didPanGestureRecognizer(_ sender: UIPanGestureRecognizer) {
|
||||
guard let window = window else {
|
||||
return
|
||||
}
|
||||
let point: CGPoint = sender.translation(in: viewController.view)
|
||||
window.center = CGPoint(x: window.center.x + point.x, y: window.center.y + point.y)
|
||||
origin = window.frame.origin
|
||||
sender.setTranslation(.zero, in: viewController.view)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue