Commit 71fa75a1 authored by Kateryna Kostiuk's avatar Kateryna Kostiuk

conversation: record audio/video

Change-Id: I650c02cbb217e9608ac13adde2f17671727f7850
parent 993d3bed
......@@ -242,6 +242,9 @@
1ABE07DC1F0D915100D36361 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1ABE07DA1F0D915100D36361 /* Localizable.strings */; };
1ABE07DF1F0D91A800D36361 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1ABE07DD1F0D91A800D36361 /* LaunchScreen.storyboard */; };
1ABE07E21F0D924700D36361 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ABE07E11F0D924700D36361 /* Strings.swift */; };
446FAF192373424700519C4F /* SendFileViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 446FAF182373424700519C4F /* SendFileViewController.storyboard */; };
446FAF1B2373425E00519C4F /* SendFileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 446FAF1A2373425E00519C4F /* SendFileViewController.swift */; };
446FAF1D2373427100519C4F /* SendFileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 446FAF1C2373427100519C4F /* SendFileViewModel.swift */; };
5516C29F1E71CEFF009D3D2D /* AccountModelHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5516C29E1E71CEFF009D3D2D /* AccountModelHelper.swift */; };
5557FD4A1E81AE850043E394 /* AccountModelHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5557FD491E81AE850043E394 /* AccountModelHelperTests.swift */; };
557086521E8ADB9D001A7CE4 /* SystemAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 557086511E8ADB9D001A7CE4 /* SystemAdapter.mm */; };
......@@ -574,6 +577,9 @@
1ABE07DB1F0D915100D36361 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
1ABE07DD1F0D91A800D36361 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Resources/LaunchScreen.storyboard; sourceTree = "<group>"; };
1ABE07E11F0D924700D36361 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
446FAF182373424700519C4F /* SendFileViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = SendFileViewController.storyboard; sourceTree = "<group>"; };
446FAF1A2373425E00519C4F /* SendFileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendFileViewController.swift; sourceTree = "<group>"; };
446FAF1C2373427100519C4F /* SendFileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendFileViewModel.swift; sourceTree = "<group>"; };
5516C29E1E71CEFF009D3D2D /* AccountModelHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountModelHelper.swift; sourceTree = "<group>"; };
5557FD491E81AE850043E394 /* AccountModelHelperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountModelHelperTests.swift; sourceTree = "<group>"; };
557086501E8ADB9D001A7CE4 /* SystemAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SystemAdapter.h; sourceTree = "<group>"; };
......@@ -1358,6 +1364,7 @@
1A2D18A71F290FAA00B2C785 /* Conversations */ = {
isa = PBXGroup;
children = (
446FAF172373421500519C4F /* SendFile */,
1A2D18AE1F29153F00B2C785 /* Conversation */,
1A2D18AD1F29151E00B2C785 /* Smartlist */,
1A2D18A91F29131900B2C785 /* ConversationsCoordinator.swift */,
......@@ -1520,6 +1527,16 @@
path = Resources;
sourceTree = "<group>";
};
446FAF172373421500519C4F /* SendFile */ = {
isa = PBXGroup;
children = (
446FAF182373424700519C4F /* SendFileViewController.storyboard */,
446FAF1A2373425E00519C4F /* SendFileViewController.swift */,
446FAF1C2373427100519C4F /* SendFileViewModel.swift */,
);
path = SendFile;
sourceTree = "<group>";
};
563AEC731EA6627F003A5641 /* NameRegistration */ = {
isa = PBXGroup;
children = (
......@@ -1803,6 +1820,7 @@
1A0C4EDA1F1D4B1B00550433 /* WelcomeViewController.storyboard in Resources */,
5CE66F751FBF769B00EE9291 /* InitialLoadingViewController.storyboard in Resources */,
1A5DC03E1F35678D0075E8EF /* ContactRequestsViewController.storyboard in Resources */,
446FAF192373424700519C4F /* SendFileViewController.storyboard in Resources */,
0E72374A20460320006B0C7D /* ProfileHeaderView.xib in Resources */,
0E96ED75225D06250016C07D /* GeneralSettingsViewController.storyboard in Resources */,
0EB1A5CF1F8EBE03009923E2 /* DeviceCell.xib in Resources */,
......@@ -1930,6 +1948,7 @@
1A2D18C51F29180700B2C785 /* ContactModel.swift in Sources */,
0E3697A8203243D3009A68CA /* BannedContactCell.swift in Sources */,
1A2D18F71F292D7200B2C785 /* MessageCellSent.swift in Sources */,
446FAF1B2373425E00519C4F /* SendFileViewController.swift in Sources */,
04399AAC1D1C304300E99CD9 /* AccountAdapter.mm in Sources */,
0E68571120238546008B0717 /* ConversationNavigation.swift in Sources */,
0E49096C1FEAB225005CAA50 /* CallsAdapterDelegate.swift in Sources */,
......@@ -2027,6 +2046,7 @@
62AA15C31FFC39C80064A063 /* VideoAdapterDelegate.swift in Sources */,
04399AAE1D1C304300E99CD9 /* Utils.mm in Sources */,
56BBC9A31ED714DF00CDAF8B /* ConversationsService.swift in Sources */,
446FAF1D2373427100519C4F /* SendFileViewModel.swift in Sources */,
62AD584A2056DADF00AF0701 /* MessageCellDataTransferReceived.swift in Sources */,
0E0FF1AF1FC38CBC003898C2 /* ProfileDataHelper.swift in Sources */,
563AEC771EA664C0003A5641 /* RegistrationResponse.m in Sources */,
......
......@@ -23,6 +23,7 @@
extern "C" {
#include <libavutil/frame.h>
#include <libavutil/display.h>
#include <libavutil/time.h>
}
@implementation Utils
......
......@@ -34,7 +34,8 @@
- (void)removeSinkTargetWithSinkId:(NSString*)sinkId;
- (void)writeOutgoingFrameWithBuffer:(CVImageBufferRef)image
angle:(int)angle
useHardwareAcceleration:(BOOL)hardwareAccelerated;
useHardwareAcceleration:(BOOL)hardwareAccelerated
recording:(BOOL)recording;
- (void)setDecodingAccelerated:(BOOL)state;
- (BOOL)getDecodingAccelerated;
- (void)switchInput:(NSString*)deviceName;
......@@ -42,5 +43,9 @@
- (void)setEncodingAccelerated:(BOOL)state;
- (BOOL)getEncodingAccelerated;
- (void)stopAudioDevice;
- (NSString*)startLocalRecording:(NSString*) path audioOnly:(BOOL)audioOnly;
- (void)stopLocalRecording:(NSString*) path;
- (void)startCamera;
- (void)stopCamera;
@end
......@@ -185,13 +185,14 @@ static id <VideoAdapterDelegate> _delegate;
- (void)writeOutgoingFrameWithBuffer:(CVImageBufferRef)image
angle:(int)angle
useHardwareAcceleration:(BOOL)hardwareAccelerated {
useHardwareAcceleration:(BOOL)hardwareAccelerated
recording:(BOOL)recording {
auto frame = DRing::getNewFrame();
if(!frame) {
return;
}
auto avframe = frame->pointer();
if(hardwareAccelerated) {
if(hardwareAccelerated && !recording) {
[Utils configureHardwareDecodedFrame:(AVFrame*)avframe
fromImageBuffer:image
angle:(int) angle];
......@@ -244,6 +245,22 @@ static id <VideoAdapterDelegate> _delegate;
DRing::stopAudioDevice();
}
- (NSString* )startLocalRecording:(NSString*) path audioOnly:(BOOL)audioOnly {
return @(DRing::startLocalRecorder(audioOnly, std::string([path UTF8String])).c_str());
}
- (void)stopLocalRecording:(NSString*) path {
DRing::stopLocalRecorder(std::string([path UTF8String]));
}
- (void)startCamera {
DRing::startCamera();
}
- (void)stopCamera {
DRing::stopCamera();
}
#pragma mark PresenceAdapterDelegate
+ (id <VideoAdapterDelegate>)delegate {
......
// swiftlint:disable all
// Generated using SwiftGen, by O.Halligon — https://github.com/SwiftGen/SwiftGen
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
#if os(OSX)
import AppKit.NSImage
......
// swiftlint:disable all
// Generated using SwiftGen, by O.Halligon — https://github.com/SwiftGen/SwiftGen
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
import Foundation
......@@ -293,6 +293,8 @@ internal enum L10n {
}
internal enum DataTransfer {
/// Press to start recording
internal static let infoMessage = L10n.tr("Localizable", "dataTransfer.infoMessage")
/// Accept
internal static let readableStatusAccept = L10n.tr("Localizable", "dataTransfer.readableStatusAccept")
/// Pending…
......@@ -313,6 +315,8 @@ internal enum L10n {
internal static let readableStatusSuccess = L10n.tr("Localizable", "dataTransfer.readableStatusSuccess")
/// Failed to send
internal static let sendingFailed = L10n.tr("Localizable", "dataTransfer.sendingFailed")
/// Send
internal static let sendMessage = L10n.tr("Localizable", "dataTransfer.sendMessage")
}
internal enum GeneralSettings {
......@@ -454,6 +458,7 @@ internal enum L10n {
extension L10n {
fileprivate static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
// swiftlint:disable:next nslocalizedstring_key
let format = NSLocalizedString(key, tableName: table, bundle: Bundle(for: BundleToken.self), comment: "")
return String(format: format, locale: Locale.current, arguments: args)
}
......
......@@ -94,12 +94,22 @@ class ConversationViewController: UIViewController,
self.importImage()
}
let recordVideoAction = UIAlertAction(title: "Record a video message", style: UIAlertAction.Style.default) { _ in
self.viewModel.recordVideoFile()
}
let recordAudioAction = UIAlertAction(title: "Record an audio message", style: UIAlertAction.Style.default) { _ in
self.viewModel.recordAudioFile()
}
let documentsAction = UIAlertAction(title: "Upload file", style: UIAlertAction.Style.default) { _ in
self.importDocument()
}
let cancelAction = UIAlertAction(title: L10n.Alerts.profileCancelPhoto, style: UIAlertAction.Style.cancel)
alert.addAction(pictureAction)
alert.addAction(recordVideoAction)
alert.addAction(recordAudioAction)
alert.addAction(documentsAction)
alert.addAction(cancelAction)
alert.popoverPresentationController?.sourceView = self.view
......
......@@ -390,6 +390,14 @@ class ConversationViewModel: Stateable, ViewModel {
self.stateSubject.onNext(ConversationState.contactDetail(conversationViewModel: self.conversation.value))
}
func recordVideoFile() {
self.stateSubject.onNext(ConversationState.recordFile(conversation: self.conversation.value, audioOnly: false))
}
func recordAudioFile() {
self.stateSubject.onNext(ConversationState.recordFile(conversation: self.conversation.value, audioOnly: true))
}
func sendFile(filePath: String, displayName: String, localIdentifier: String? = nil) {
guard let accountId = accountService.currentAccount?.id else {return}
self.dataTransferService.sendFile(filePath: filePath,
......
/*
* Copyright (C) 2019 Savoir-faire Linux Inc.
*
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import UIKit
import RxSwift
import Reusable
import SwiftyBeaver
class SendFileViewController: UIViewController, StoryboardBased, ViewModelBased {
var viewModel: SendFileViewModel!
fileprivate let disposeBag = DisposeBag()
private let log = SwiftyBeaver.self
@IBOutlet weak var preview: UIImageView!
@IBOutlet weak var recordButton: UIButton!
@IBOutlet weak var sendButton: UIButton!
@IBOutlet weak var cancelButton: UIButton!
@IBOutlet weak var timerLabel: UILabel!
@IBOutlet weak var infoLabel: UILabel!
@IBOutlet weak var placeholderLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
self.applyL10()
self.viewModel.capturedFrame
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] frame in
if let image = frame {
DispatchQueue.main.async {
self?.preview.image = image
}
}
}).disposed(by: self.disposeBag)
self.cancelButton.rx.tap
.subscribe(onNext: { [unowned self] in
self.viewModel.cancel()
}).disposed(by: self.disposeBag)
self.recordButton.rx.tap
.subscribe(onNext: { [unowned self] in
self.viewModel.triggerRecording()
}).disposed(by: self.disposeBag)
self.sendButton.rx.tap
.subscribe(onNext: { [unowned self] in
self.viewModel.sendFile()
}).disposed(by: self.disposeBag)
self.viewModel.hidePreview
.observeOn(MainScheduler.instance)
.bind(to: self.preview.rx.isHidden)
.disposed(by: self.disposeBag)
self.viewModel.readyToSend
.map {!$0}
.drive(self.sendButton.rx.isHidden)
.disposed(by: self.disposeBag)
self.viewModel.duration
.drive(self.timerLabel.rx.text)
.disposed(by: self.disposeBag)
self.viewModel.finished
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] finished in
if finished {
self?.dismiss(animated: true, completion: nil)
}
}).disposed(by: self.disposeBag)
self.viewModel.readyToSend
.drive(self.placeholderLabel.rx.isHidden)
.disposed(by: self.disposeBag)
self.viewModel.recording
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] recording in
if recording {
UIView.animate(withDuration: 1,
delay: 0.0,
options: [.curveEaseInOut,
.allowUserInteraction,
.autoreverse,
.repeat],
animations: { [weak self] in
self?.recordButton.alpha = 0.1
},
completion: { [weak self] _ in
self?.recordButton.alpha = 1.0
})
} else {
self?.recordButton.layer.removeAllAnimations()
}
}).disposed(by: self.disposeBag)
self.viewModel.hideInfo
.drive(self.infoLabel.rx.isHidden)
.disposed(by: self.disposeBag)
}
func applyL10() {
self.sendButton.setTitle(L10n.DataTransfer.sendMessage, for: .normal)
self.cancelButton.setTitle(L10n.Actions.cancelAction, for: .normal)
self.infoLabel.text = L10n.DataTransfer.infoMessage
}
}
/*
* Copyright (C) 2019 Savoir-faire Linux Inc.
*
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import RxSwift
import SwiftyBeaver
import Contacts
import RxCocoa
enum RecordingState {
case initial
case recording
case recorded
case sent
}
class SendFileViewModel: Stateable, ViewModel {
//stateable
private let stateSubject = PublishSubject<State>()
lazy var state: Observable<State> = {
return self.stateSubject.asObservable()
}()
private let recordingState = Variable<RecordingState>(.initial)
lazy var capturedFrame: Observable<UIImage?> = {
if !audioOnly {
videoService.prepareVideoRecording()
}
return videoService.capturedVideoFrame.asObservable().map({ frame in
return frame
})
}()
lazy var hidePreview: Observable<Bool> = {
Observable.just(audioOnly)
}()
lazy var finished: Observable<Bool> = {
recordingState
.asObservable()
.map({ state in
state == .sent
}).share()
}()
lazy var hideInfo: Driver<Bool> = {
recordingState
.asObservable()
.map({ state in
state != .initial
}).share()
.asDriver(onErrorJustReturn: false)
}()
lazy var readyToSend: Driver<Bool> = {
recordingState
.asObservable()
.map({ state in
state == .recorded
}).share()
.asDriver(onErrorJustReturn: false)
}()
lazy var recording: Observable<Bool> = {
recordingState
.asObservable()
.map({ state in
state == .recording
}).share()
}()
lazy var duration: Driver<String> = {
let durationTimer = Observable<Int>
.interval(1.0, scheduler: MainScheduler.instance)
.takeUntil(self.recordingState
.asObservable()
.filter { state in
return state == .recorded
})
.map({ interval -> String in
let seconds = interval % 60
let minutes = (interval / 60) % 60
let hours = (interval / 3600)
switch hours {
case 0:
return String(format: "%02d:%02d", minutes, seconds)
default:
return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
}
}).share()
return self.recordingState
.asObservable()
.filter({ state in
return state == .recording
}).flatMap({ _ in
return durationTimer
}).asDriver(onErrorJustReturn: "")
}()
var audioOnly: Bool = false
fileprivate let videoService: VideoService
fileprivate let accountService: AccountsService
fileprivate let fileTransferService: DataTransferService
var fileName = ""
var conversation: ConversationModel!
required init(with injectionBag: InjectionBag) {
self.videoService = injectionBag.videoService
self.accountService = injectionBag.accountService
self.fileTransferService = injectionBag.dataTransferService
}
func triggerRecording() {
if recordingState.value == .recording {
self.stopRecording()
return
}
startRecording()
}
func startRecording() {
guard let name = self.videoService
.startLocalRecorder(audioOnly: audioOnly) else {
return
}
recordingState.value = .recording
fileName = name
}
func stopRecording() {
self.videoService.stopLocalRecorder(path: fileName)
recordingState.value = .recorded
}
func sendFile() {
guard let fileUrl = URL(string: fileName) else {
return
}
let name = fileUrl.lastPathComponent
guard let accountId = accountService.currentAccount?.id else {return}
self.fileTransferService.sendFile(filePath: fileName,
displayName: name,
accountId: accountId,
peerInfoHash: self.conversation.hash,
localIdentifier: nil)
self.videoService.videRecordingFinished()
recordingState.value = .sent
}
func cancel() {
if recordingState.value == .recording {
self.stopRecording()
}
self.videoService.videRecordingFinished()
recordingState.value = .sent
}
}
......@@ -30,6 +30,7 @@ enum ConversationState: State {
case createNewAccount
case showDialpad(inCall: Bool)
case showGeneralSettings
case recordFile(conversation: ConversationModel, audioOnly: Bool)
case navigateToCall(call: CallModel)
}
......@@ -55,12 +56,24 @@ extension ConversationNavigation where Self: Coordinator, Self: StateableRespons
self.presentContactInfo(conversation: conversationModel)
case .qrCode:
self.openQRCode()
case .recordFile(let conversation, let audioOnly):
self.openRecordFile(conversation: conversation, audioOnly: audioOnly)
default:
break
}
}).disposed(by: self.disposeBag)
}
func openRecordFile(conversation: ConversationModel, audioOnly: Bool) {
let recordFileViewController = SendFileViewController.instantiate(with: self.injectionBag)
recordFileViewController.viewModel.conversation = conversation
recordFileViewController.viewModel.audioOnly = audioOnly
self.present(viewController: recordFileViewController,
withStyle: .popup,
withAnimation: true,
withStateable: recordFileViewController.viewModel)
}
func openQRCode () {
let scanViewController = ScanViewController.instantiate(with: self.injectionBag)
self.present(viewController: scanViewController,
......
......@@ -241,6 +241,8 @@
"dataTransfer.readableStatusSuccess" = "Complete";
"dataTransfer.readableStatusAccept" = "Accept";
"dataTransfer.readableStatusCancel" = "Cancel";
"dataTransfer.infoMessage" = "Press to start recording";
"dataTransfer.sendMessage" = "Send";
//Generated Message
......
......@@ -302,6 +302,8 @@ class VideoService: FrameExtractorDelegate {
fileprivate let disposeBag = DisposeBag()
var recording = false
init(withVideoAdapter videoAdapter: VideoAdapter) {
self.videoAdapter = videoAdapter
currentOrientation = camera.getOrientation
......@@ -448,6 +450,15 @@ extension VideoService: VideoAdapterDelegate {
self.camera.startCapturing()
}
func prepareVideoRecording() {
self.videoAdapter.startCamera()
}
func videRecordingFinished() {
self.videoAdapter.stopCamera()
self.stopAudioDevice()
}
func stopCapture() {
self.log.debug("Capture stopped...")
self.camera.stopCapturing()
......@@ -481,7 +492,8 @@ extension VideoService: VideoAdapterDelegate {
}
videoAdapter.writeOutgoingFrame(with: imageBuffer,
angle: Int32(self.angle),
useHardwareAcceleration: self.hardwareAccelerated)
useHardwareAcceleration: self.hardwareAccelerated,
recording: self.recording)
}
func updateDevicePisition(position: AVCaptureDevice.Position) {
......@@ -491,4 +503,36 @@ extension VideoService: VideoAdapterDelegate {
func stopAudioDevice() {
videoAdapter.stopAudioDevice()
}
func startLocalRecorder(audioOnly: Bool) -> String? {
//configure path
let recordingsFolderName = "recorded"
guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
return nil
}
let directoryURL = documentsURL.appendingPathComponent(recordingsFolderName)
var isDirectory = ObjCBool(false)
let directoryExists = FileManager.default.fileExists(atPath: directoryURL.path, isDirectory: &isDirectory)
if !directoryExists || !isDirectory.boolValue {
do {
try FileManager.default.createDirectory(atPath: directoryURL.path, withIntermediateDirectories: true, attributes: nil)
} catch _ as NSError {
return nil
}
}
let dateFormatter: DateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd_HH:mm:ss"
let date = Date()
let dateString = dateFormatter.string(from: date)
let random = String(arc4random_uniform(9999))
let fileName = dateString + "_" + random
let videoURL = directoryURL.appendingPathComponent(fileName, isDirectory: false)
self.recording = true
return self.videoAdapter.startLocalRecording(videoURL.path, audioOnly: audioOnly)
}
func stopLocalRecorder(path: String) {
self.videoAdapter.stopLocalRecording(path)
self.recording = false
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment