Skip to content

Commit

Permalink
Merge pull request #83 from AgoraIO-Community/custom-camera
Browse files Browse the repository at this point in the history
Easy Custom Camera
  • Loading branch information
maxxfrazer authored Nov 25, 2022
2 parents 5206e11 + 4220511 commit 92f6278
Show file tree
Hide file tree
Showing 16 changed files with 685 additions and 214 deletions.
1 change: 1 addition & 0 deletions .github/workflows/swift-build-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ jobs:
pod lib lint AgoraBroadcastExtensionHelper_iOS.podspec --allow-warnings --skip-import-validation --include-podspecs='AgoraAppGroupDataHelper_iOS.podspec';
- name: Print Version 🔤
run: |
export LIB_VERSION=$(grep 'static let version' Sources/Agora-Video-UIKit/AgoraUIKit.swift | sed -e 's,.*\"\(.*\)\",\1,')
echo '### Build passed :rocket:' >> $GITHUB_STEP_SUMMARY
echo "Version: $LIB_VERSION" >> $GITHUB_STEP_SUMMARY
257 changes: 257 additions & 0 deletions Sources/Agora-Video-UIKit/AgoraCameraSourcePush.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
//
// AgoraCameraSourcePush.swift
// Agora-UIKit-Example
//
// Created by Max Cobb on 22/09/2022.
//

#if os(iOS)
import UIKit
#elseif os(macOS)
import AppKit
#endif
import AVFoundation
import AgoraRtcKit

#if os(iOS)
internal extension UIDeviceOrientation {
func toCaptureVideoOrientation() -> AVCaptureVideoOrientation {
switch self {
case .portrait: return .portrait
case .portraitUpsideDown: return .portraitUpsideDown
case .landscapeLeft: return .landscapeLeft
case .landscapeRight: return .landscapeRight
default: return .portrait
}
}
var intRotation: Int {
switch self {
case .portrait: return 90
case .landscapeLeft: return 0
case .landscapeRight: return 180
case .portraitUpsideDown: return -90
default: return 90
}
}
}
#endif

/// View to show the custom camera feed for the local camera feed.
open class CustomVideoSourcePreview: MPView {
/// Layer that displays video from a camera device.
open private(set) var previewLayer: AVCaptureVideoPreviewLayer?

/// Add new frame to the preview layer
/// - Parameter previewLayer: New `previewLayer` to be displayed on the preview.
open func insertCaptureVideoPreviewLayer(previewLayer: AVCaptureVideoPreviewLayer) {
self.previewLayer?.removeFromSuperlayer()
#if os(macOS)
guard let layer = layer else { return }
#endif
previewLayer.frame = bounds
layer.insertSublayer(previewLayer, below: layer.sublayers?.first)
self.previewLayer = previewLayer
}

#if os(iOS)
/// Tells the delegate a layer's bounds have changed.
/// - Parameter layer: The layer that requires layout of its sublayers.
override open func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
previewLayer?.frame = bounds
if let connection = self.previewLayer?.connection {
let currentDevice = UIDevice.current
let orientation: UIDeviceOrientation = currentDevice.orientation
let previewLayerConnection: AVCaptureConnection = connection

if previewLayerConnection.isVideoOrientationSupported {
self.updatePreviewLayer(
layer: previewLayerConnection,
orientation: orientation.toCaptureVideoOrientation()
)
}
}
}
#elseif os(macOS)
open override func layout() {
super.layout()
self.previewLayer?.frame = bounds
}
#endif

private func updatePreviewLayer(layer: AVCaptureConnection, orientation: AVCaptureVideoOrientation) {
layer.videoOrientation = orientation
self.previewLayer?.frame = self.bounds
}

#if os(macOS)
public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.wantsLayer = true
}

required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#endif
}

/// Delegate for capturing the frames from the camera source.
public protocol AgoraCameraSourcePushDelegate: AnyObject {
func myVideoCapture(
_ capture: AgoraCameraSourcePush, didOutputSampleBuffer pixelBuffer: CVPixelBuffer,
rotation: Int, timeStamp: CMTime
)
}

open class AgoraCameraSourcePush: NSObject {
fileprivate var delegate: AgoraCameraSourcePushDelegate?
private var localVideoPreview: CustomVideoSourcePreview?

/// Active capture session
public let captureSession: AVCaptureSession
/// DispatchQueue for processing and sending images from ``captureSession``
public let captureQueue: DispatchQueue
/// Latest output from the active ``captureSession``.
public var currentOutput: AVCaptureVideoDataOutput? {
if let outputs = self.captureSession.outputs as? [AVCaptureVideoDataOutput] {
return outputs.first
} else {
return nil
}
}

/// Create a new AgoraCameraSourcePush object
/// - Parameters:
/// - delegate: Camera source delegate, where the pixel buffer is sent to.
/// - localVideoPreview: Local view where the camera feed is rendered to.
public init(
delegate: AgoraCameraSourcePushDelegate,
localVideoPreview: CustomVideoSourcePreview?
) {
self.delegate = delegate
self.localVideoPreview = localVideoPreview

self.captureSession = AVCaptureSession()
#if os(iOS)
self.captureSession.usesApplicationAudioSession = false
#endif

let captureOutput = AVCaptureVideoDataOutput()
captureOutput.videoSettings = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
]
if self.captureSession.canAddOutput(captureOutput) {
self.captureSession.addOutput(captureOutput)
}

self.captureQueue = DispatchQueue(label: "AgoraCaptureQueue")

let previewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession)
localVideoPreview?.insertCaptureVideoPreviewLayer(previewLayer: previewLayer)
}

/// Update the local preview layer to a new one.
/// - Parameter videoPreview: New custom preview layer.
open func updateVideoPreview(to videoPreview: CustomVideoSourcePreview) {
self.localVideoPreview?.previewLayer?.removeFromSuperlayer()

let previewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession)
videoPreview.insertCaptureVideoPreviewLayer(previewLayer: previewLayer)
self.localVideoPreview = videoPreview
}

deinit {
self.captureSession.stopRunning()
}

func changeCaptureDevice(to device: AVCaptureDevice) {
self.startCapture(ofDevice: device)
}

/// Start caturing frames from the device. Usually internally called.
/// - Parameter device: Capture device to have images captured from.
open func startCapture(ofDevice device: AVCaptureDevice) {
guard let currentOutput = self.currentOutput else {
return
}

currentOutput.setSampleBufferDelegate(self, queue: self.captureQueue)

captureQueue.async { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.setCaptureDevice(device, ofSession: strongSelf.captureSession)
strongSelf.captureSession.beginConfiguration()
if strongSelf.captureSession.canSetSessionPreset(.vga640x480) {
strongSelf.captureSession.sessionPreset = .vga640x480
}
strongSelf.captureSession.commitConfiguration()
strongSelf.captureSession.startRunning()
}
}

func resumeCapture() {
self.currentOutput?.setSampleBufferDelegate(self, queue: self.captureQueue)
self.captureQueue.async { [weak self] in
self?.captureSession.startRunning()
}
}

func stopCapture() {
self.currentOutput?.setSampleBufferDelegate(nil, queue: nil)
self.captureQueue.async { [weak self] in
self?.captureSession.stopRunning()
}
}

}

public extension AgoraCameraSourcePush {
func setCaptureDevice(_ device: AVCaptureDevice, ofSession captureSession: AVCaptureSession) {
let currentInputs = captureSession.inputs as? [AVCaptureDeviceInput]
let currentInput = currentInputs?.first

if let currentInputName = currentInput?.device.localizedName,
currentInputName == device.uniqueID {
return
}

guard let newInput = try? AVCaptureDeviceInput(device: device) else {
return
}

captureSession.beginConfiguration()
if let currentInput = currentInput {
captureSession.removeInput(currentInput)
}
if captureSession.canAddInput(newInput) {
captureSession.addInput(newInput)
}
captureSession.commitConfiguration()
}
}

extension AgoraCameraSourcePush: AVCaptureVideoDataOutputSampleBufferDelegate {
open func captureOutput(
_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
DispatchQueue.main.async {[weak self] in
guard let weakSelf = self else { return }

#if os(iOS)
let imgRot = UIDevice.current.orientation.intRotation
#else
let imgRot = 0
#endif
weakSelf.delegate?.myVideoCapture(
weakSelf, didOutputSampleBuffer: pixelBuffer,
rotation: imgRot, timeStamp: time
)
}
}
}
11 changes: 10 additions & 1 deletion Sources/Agora-Video-UIKit/AgoraSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import AgoraRtcKit
#if canImport(AgoraRtmControl)
import AgoraRtmKit
#endif
import AVKit

/// Settings used for the display and behaviour of AgoraVideoViewer
public struct AgoraSettings {
Expand Down Expand Up @@ -101,15 +102,23 @@ public struct AgoraSettings {
/// - `false`: The external video source is not encoded.
public let encoded: Bool

/// Class for the logic around using a custom camera.
public var captureDevice: AVCaptureDevice?

/// Create a settings object for applying external videos
/// - Parameters:
/// - enabled: Determines whether to enable the external video source.
/// - texture: Determines whether to use textured video data.
/// - encoded: Determines whether the external video source is encoded.
public init(enabled: Bool, texture: Bool, encoded: Bool) {
/// - customCamera: Class for the logic around using a custom camera.
public init(
enabled: Bool, texture: Bool, encoded: Bool,
captureDevice: AVCaptureDevice? = nil
) {
self.enabled = enabled
self.texture = texture
self.encoded = encoded
self.captureDevice = captureDevice
}
}

Expand Down
24 changes: 24 additions & 0 deletions Sources/Agora-Video-UIKit/AgoraSingleVideoView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class AgoraSingleVideoView: MPView {
didSet {
if oldValue != videoMuted {
self.canvas.view?.isHidden = videoMuted
self.customCameraView?.isHidden = videoMuted
}
self.updateUserOptions()
}
Expand Down Expand Up @@ -54,6 +55,29 @@ public class AgoraSingleVideoView: MPView {
self.canvas.view
}

var customCameraView: CustomVideoSourcePreview? {
didSet {
if let oldValue = oldValue {
oldValue.removeFromSuperview()
}
if let customCameraView = customCameraView {
if let defaultCamView = self.canvas.view {
#if os(iOS)
self.insertSubview(customCameraView, aboveSubview: defaultCamView)
#elseif os(macOS)
self.addSubview(customCameraView, positioned: .above, relativeTo: defaultCamView)
#endif
}
customCameraView.frame = self.bounds
#if os(iOS)
customCameraView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
#elseif os(macOS)
customCameraView.autoresizingMask = [.width, .height]
#endif
}
}
}

var micFlagColor: MPColor

enum UserOptions: String {
Expand Down
6 changes: 5 additions & 1 deletion Sources/Agora-Video-UIKit/AgoraUIKit.docc/AgoraUIKit.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Integrate Agora Video Calling or Live Video Streaming to your iOS or macOS app w

## Overview

Agora UIKit is a low-code solution to adding video calls and live streams into your application.
Agora Video UI Kit is a low-code solution to adding video calls and live streams into your application.

Get started with this package by creating an ``AgoraVideoViewer`` and joining a channel:

Expand All @@ -23,6 +23,10 @@ let agoraView = AgoraVideoViewer(
agoraView.join(channel: "test", as: .broadcaster)
```

- ``AgoraVideoViewer/init(connectionData:style:agoraSettings:delegate:)``
- ``AgoraConnectionData``
- ``AgoraVideoViewer/join(channel:with:as:uid:mediaOptions:)``

### Getting Started

- <doc:Quickstart-UIKit>
Loading

0 comments on commit 92f6278

Please sign in to comment.