├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json └── src ├── Commands ├── AppleScripts │ ├── browser_googlechrome_reset.scpt │ ├── browser_googlechrome_reset.txt │ ├── browser_safari_clearHistory │ ├── browser_safari_clearHistory.scpt │ ├── getVolume.scpt │ ├── getVolume.txt │ ├── iTunesPlaylist.scpt │ ├── iTunesPlaylist.txt │ ├── isMuted.scpt │ ├── isMuted.txt │ ├── logout.scpt │ ├── logout.txt │ ├── notify.scpt │ ├── notify.txt │ ├── setVolume.scpt │ ├── setVolume.txt │ ├── showAlert.scpt │ └── showAlert.txt ├── SwiftScripts │ ├── Recorder │ │ ├── CamRecorder │ │ │ ├── Recorder │ │ │ └── main.swift │ │ ├── Codec │ │ │ ├── AACEncoder.swift │ │ │ ├── H264Decoder.swift │ │ │ └── H264Encoder.swift │ │ ├── Core │ │ │ ├── Constant.swift │ │ │ └── Protocol.swift │ │ ├── Extension │ │ │ ├── CMAudioFormatDescription+Extension.swift │ │ │ ├── CMBlockBuffer+Extension.swift │ │ │ ├── CMFormatDescription+Extension.swift │ │ │ ├── CMSampleBuffer+Extension.swift │ │ │ ├── CMSampleTimingInfo+Extension.swift │ │ │ ├── CMVideoFormatDescription+Extension.swift │ │ │ ├── CVPixelBuffer+Extension.swift │ │ │ ├── Data+Extension.swift │ │ │ ├── ExpressibleByIntegerLiteral+Extension.swift │ │ │ ├── Mirror+Extension.swift │ │ │ ├── URL+Extension.swift │ │ │ └── VTSession+Extension.swift │ │ ├── HTTP │ │ │ ├── HTTPRequest.swift │ │ │ ├── HTTPResponse.swift │ │ │ ├── HTTPService.swift │ │ │ ├── HTTPStream.swift │ │ │ └── M3U.swift │ │ ├── ISO │ │ │ ├── AudioSpecificConfig.swift │ │ │ ├── H264+AVC.swift │ │ │ ├── MP4Reader.swift │ │ │ ├── MP4Sampler.swift │ │ │ ├── NALUnit.swift │ │ │ ├── PacketizedElementaryStream.swift │ │ │ ├── ProgramSpecific.swift │ │ │ ├── TSReader.swift │ │ │ ├── TSWriter.swift │ │ │ └── TransportStream.swift │ │ ├── Media │ │ │ ├── AVMixer.swift │ │ │ ├── AVMixerRecorder.swift │ │ │ ├── AudioIOComponent.swift │ │ │ ├── AudioStreamPlayback.swift │ │ │ ├── IOComponent.swift │ │ │ ├── ScalingMode.swift │ │ │ ├── SoundSpliter.swift │ │ │ ├── SoundTransform.swift │ │ │ ├── VideoIOComponent.swift │ │ │ └── VisualEffect.swift │ │ ├── Net │ │ │ ├── MIME.swift │ │ │ ├── NetClient.swift │ │ │ ├── NetService.swift │ │ │ ├── NetSocket.swift │ │ │ └── NetStream.swift │ │ ├── RTMP │ │ │ ├── AMF0Serializer.swift │ │ │ ├── AMF3Serializer.swift │ │ │ ├── ASClass.swift │ │ │ ├── RTMPChunk.swift │ │ │ ├── RTMPConnection.swift │ │ │ ├── RTMPHandshake.swift │ │ │ ├── RTMPMessage.swift │ │ │ ├── RTMPMuxer.swift │ │ │ ├── RTMPSharedObject.swift │ │ │ ├── RTMPSocket.swift │ │ │ ├── RTMPStream.swift │ │ │ ├── RTMPStreamQoSDelagate.swift │ │ │ └── RTMPTSocket.swift │ │ ├── ScreenRecorder │ │ │ ├── Recorder │ │ │ └── main.swift │ │ ├── Util │ │ │ ├── AnyUtil.swift │ │ │ ├── ByteArray.swift │ │ │ ├── CRC32.swift │ │ │ ├── DeviceUtil.swift │ │ │ ├── DisplayLinkedQueue.swift │ │ │ ├── EventDispatcher.swift │ │ │ ├── MD5.swift │ │ │ ├── MachUtil.swift │ │ │ ├── Mutex.swift │ │ │ ├── TimerDriver.swift │ │ │ └── VideoGravityUtil.swift │ │ └── macOS │ │ │ ├── AudioUtil.swift │ │ │ ├── NetStream+Extenstion.swift │ │ │ └── VideoIOComponent+Extension.swift │ ├── showPairing.swift │ └── toggleBluetooth.swift └── commands.js ├── Routes ├── BluetoothRouter.js ├── BrowserRouter.js ├── MusicRouter.js ├── PowerRouter.js ├── SystemSpyRouter.js ├── SystemStatsRouter.js └── WifiRouter.js ├── Services ├── CommandService.js ├── RouterService.js └── SessionService.js ├── Utilities └── logger.js ├── environment.js ├── routes.js ├── server.js └── test.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored 2 | *.js linguist-vendored=false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | sudo: false 5 | os: 6 | - osx 7 | install: 8 | - npm install --ignore-scripts 9 | script: 10 | - npm test 11 | notifications: 12 | slack: 13 | secure: rSr7PKcWh/RYrUTpytUnV87gZmpU093Qtdry6HJ2o3FyYhm4jdPbiGKbh84VqZT+/4XMWg+x+RTy+KVxbjiB8Pgglh8KVAD1UdFQamOWzCcVtsPBF9VAgEIIi7Hc/ESylc8TOF/2arjTyNkdARMALO9nZ3yI/PqFbNvFrTYFmaKwA3hqKeNO7lzSJAV5761bgjbv0Rj2G/DNytRI3VIL9eDnJXLnLBGno2tAEhNWsDx+P/CqyDw0XikLaBebNUeOerK4GmzANBLwgDFTqICs2zAZs5Llwneb+Zgx3Xo9v0aANAIXHp7agIALMdTg/dD1smZrXau8AiojP3FYz/56746QAtdgTgMN5VYUlV4X/IThQbFDJYl7W9K0bdrVhqMbROQ7BI06cp27T5swH3XxiTtt8KI6B2wLR5AWsfu5ZT1rexpseNDCwbD87TiqhepWONRLL8pXNwiEMhVA8LiqxAyihhCXgQldgG/ylHvq4m1TKcK71aBjJSQyN/Dv/T+1wYKQT7S+u+BB+uHU17mPXDOYC6FnKRXQgWrZDyz2VADNW/n5FJZrSYYjbU3xJ845nJKUr5vXhILAHaHHtnMc9jiKgFZ0Q9jDbHKb8iKh9J6iKIOo3M9SdaUmynFBK3HD208XScNUqfXnUxtsH+Ym4X1c57q7P9wx2ckOrVCIR+o= 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mail@sahilchaddha.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Feel free to Contribute Amigos ! 2 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Issue Tracking can be used to report bugs, issues and feature requirements. 2 | 3 | 4 | 5 | ## Expected Behavior 6 | 7 | 8 | ## Current Behavior 9 | 10 | 11 | ## Possible Solution 12 | 13 | 14 | ## Steps to Reproduce 15 | 16 | 17 | 1. 18 | 2. 19 | 3. 20 | 4. 21 | 22 | ## Context (Environment) 23 | 24 | 25 | 26 | 27 | 28 | ## Detailed Description 29 | 30 | 31 | ## Possible Implementation 32 | 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unix-remotecontrol", 3 | "version": "0.4.0", 4 | "preferGlobal": "true", 5 | "description": "UnixControl -> Control Your MBP remotely", 6 | "main": "src/server.js", 7 | "scripts": { 8 | "start": "node src/server.js", 9 | "test": "node src/test.js", 10 | "postinstall": "brew install blueutil" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/sahilchaddha/unix-remotecontrol.git" 15 | }, 16 | "keywords": [ 17 | "mac", 18 | "remote", 19 | "control", 20 | "macos", 21 | "linux", 22 | "remotecontrol", 23 | "iot", 24 | "rasberrypi" 25 | ], 26 | "author": "Sahil Chaddha", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/sahilchaddha/unix-remotecontrol/issues" 30 | }, 31 | "homepage": "https://github.com/sahilchaddha/unix-remotecontrol#readme", 32 | "dependencies": { 33 | "express": "^4.16.2", 34 | "imagesnapjs": "0.0.7", 35 | "my-local-ip": "^1.0.0", 36 | "osx-temperature-sensor": "^1.0.1", 37 | "sudo-js": "^1.0.2", 38 | "systeminformation": "^3.37.4" 39 | }, 40 | "devDependencies": { 41 | "supertest": "^3.0.0" 42 | }, 43 | "bin": { 44 | "unixremote": "./src/server.js" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Commands/AppleScripts/browser_googlechrome_reset.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahilchaddha/unix-remotecontrol/19e731748115abb017f5221bffeea3e52eb8d6cf/src/Commands/AppleScripts/browser_googlechrome_reset.scpt -------------------------------------------------------------------------------- /src/Commands/AppleScripts/browser_googlechrome_reset.txt: -------------------------------------------------------------------------------- 1 | tell application "Google Chrome" to quit 2 | do shell script "rm -rf ~/Library/Application\\ Support/Google/Chrome/Default" -------------------------------------------------------------------------------- /src/Commands/AppleScripts/browser_safari_clearHistory: -------------------------------------------------------------------------------- 1 | activate application "Safari" 2 | tell application "System Events" 3 | tell process "Safari" 4 | set frontmost to true 5 | click menu "History" of menu bar 1 6 | click menu item "Clear History…" of menu "History" of menu bar 1 7 | click (button "Clear History" of sheet 1 of window "Favorites" of application process "Safari" of application "System Events") 8 | end tell 9 | end tell 10 | 11 | -------------------------------------------------------------------------------- /src/Commands/AppleScripts/browser_safari_clearHistory.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahilchaddha/unix-remotecontrol/19e731748115abb017f5221bffeea3e52eb8d6cf/src/Commands/AppleScripts/browser_safari_clearHistory.scpt -------------------------------------------------------------------------------- /src/Commands/AppleScripts/getVolume.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahilchaddha/unix-remotecontrol/19e731748115abb017f5221bffeea3e52eb8d6cf/src/Commands/AppleScripts/getVolume.scpt -------------------------------------------------------------------------------- /src/Commands/AppleScripts/getVolume.txt: -------------------------------------------------------------------------------- 1 | get volume settings 2 | set theOutput to output volume of result 3 | return theOutput -------------------------------------------------------------------------------- /src/Commands/AppleScripts/iTunesPlaylist.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahilchaddha/unix-remotecontrol/19e731748115abb017f5221bffeea3e52eb8d6cf/src/Commands/AppleScripts/iTunesPlaylist.scpt -------------------------------------------------------------------------------- /src/Commands/AppleScripts/iTunesPlaylist.txt: -------------------------------------------------------------------------------- 1 | on run args 2 | set playlist_name to "" & item 1 of args & "" 3 | tell application "iTunes" 4 | activate 5 | end tell 6 | delay 10 7 | tell application "iTunes" 8 | play user playlist playlist_name 9 | end tell 10 | end run -------------------------------------------------------------------------------- /src/Commands/AppleScripts/isMuted.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahilchaddha/unix-remotecontrol/19e731748115abb017f5221bffeea3e52eb8d6cf/src/Commands/AppleScripts/isMuted.scpt -------------------------------------------------------------------------------- /src/Commands/AppleScripts/isMuted.txt: -------------------------------------------------------------------------------- 1 | get volume settings 2 | set theOutput to output muted of result 3 | return theOutput -------------------------------------------------------------------------------- /src/Commands/AppleScripts/logout.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahilchaddha/unix-remotecontrol/19e731748115abb017f5221bffeea3e52eb8d6cf/src/Commands/AppleScripts/logout.scpt -------------------------------------------------------------------------------- /src/Commands/AppleScripts/logout.txt: -------------------------------------------------------------------------------- 1 | tell application "System Events" to log out 2 | -------------------------------------------------------------------------------- /src/Commands/AppleScripts/notify.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahilchaddha/unix-remotecontrol/19e731748115abb017f5221bffeea3e52eb8d6cf/src/Commands/AppleScripts/notify.scpt -------------------------------------------------------------------------------- /src/Commands/AppleScripts/notify.txt: -------------------------------------------------------------------------------- 1 | on run argv 2 | display notification item 1 of argv with title item 2 of argv 3 | end run 4 | -------------------------------------------------------------------------------- /src/Commands/AppleScripts/setVolume.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahilchaddha/unix-remotecontrol/19e731748115abb017f5221bffeea3e52eb8d6cf/src/Commands/AppleScripts/setVolume.scpt -------------------------------------------------------------------------------- /src/Commands/AppleScripts/setVolume.txt: -------------------------------------------------------------------------------- 1 | on run argv 2 | set volume item 1 of argv 3 | end run -------------------------------------------------------------------------------- /src/Commands/AppleScripts/showAlert.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahilchaddha/unix-remotecontrol/19e731748115abb017f5221bffeea3e52eb8d6cf/src/Commands/AppleScripts/showAlert.scpt -------------------------------------------------------------------------------- /src/Commands/AppleScripts/showAlert.txt: -------------------------------------------------------------------------------- 1 | on run argv 2 | tell application "System Events" to display dialog item 1 of argv 3 | end run -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/CamRecorder/Recorder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahilchaddha/unix-remotecontrol/19e731748115abb017f5221bffeea3e52eb8d6cf/src/Commands/SwiftScripts/Recorder/CamRecorder/Recorder -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/CamRecorder/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // Recorder 4 | // 5 | // Created by Sumit Chudasama on 07/03/2018. 6 | // Copyright © 2018 Sumit Chudasama. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AVFoundation 11 | import CoreMediaIO 12 | 13 | var cameraSession: AVCaptureSession = { 14 | let s = AVCaptureSession() 15 | if (s.canSetSessionPreset(AVCaptureSession.Preset.low)) { 16 | s.sessionPreset = AVCaptureSession.Preset.low 17 | } else { 18 | print("session error") 19 | exit(-1) 20 | } 21 | return s 22 | }() 23 | 24 | let videoSettings = [AVVideoCodecKey: AVVideoCodecType.h264, 25 | AVVideoWidthKey: NSNumber(value: 750), 26 | AVVideoHeightKey: NSNumber(value: 1334)] as [String : Any] 27 | 28 | let devices = AVCaptureDevice.devices(for: AVMediaType.video) 29 | let _fileOutput = AVCaptureMovieFileOutput() 30 | 31 | var httpService: HLSService = HLSService( 32 | domain: "local", type: HTTPService.type, name: "", port: HTTPService.defaultPort 33 | ) 34 | var httpStream: HTTPStream = HTTPStream() 35 | 36 | let cameras: [Any]! = AVCaptureDevice.devices(for: AVMediaType.video) 37 | for camera in cameras { 38 | if let camera: AVCaptureDevice = camera as? AVCaptureDevice, camera.localizedName == "FaceTime HD Camera" { 39 | httpStream.attachCamera(AVCaptureDevice(uniqueID: camera.uniqueID)) 40 | break 41 | } 42 | } 43 | 44 | let audios: [Any]! = AVCaptureDevice.devices(for: AVMediaType.audio) 45 | for audio in audios { 46 | if let audio: AVCaptureDevice = audio as? AVCaptureDevice, audio.localizedName == "Built-in Microphone" { 47 | httpStream.attachAudio(AVCaptureDevice(uniqueID: audio.uniqueID)) 48 | break 49 | } 50 | } 51 | 52 | print("http://localhost:8080/camRecord/playlist.m3u8") 53 | 54 | if devices.count != 0 { 55 | 56 | let input = try AVCaptureDeviceInput.init(device: devices[0]) 57 | 58 | if cameraSession.canAddInput(input) { 59 | cameraSession.addInput(input) 60 | } else { 61 | print("no input") 62 | exit(-2) 63 | } 64 | 65 | if cameraSession.canAddOutput(_fileOutput) { 66 | cameraSession.addOutput(_fileOutput) 67 | _fileOutput.setOutputSettings(videoSettings, for: _fileOutput.connections[0]) 68 | } else { 69 | print("no output") 70 | exit(-3) 71 | } 72 | 73 | cameraSession.startRunning() 74 | } 75 | 76 | class Service { 77 | @objc func stopService() { 78 | httpService.removeHTTPStream(httpStream) 79 | httpService.stopRunning() 80 | httpStream.publish(nil) 81 | print("Service Stopped") 82 | } 83 | 84 | func startService() { 85 | httpStream.publish("camRecord") 86 | httpService.addHTTPStream(httpStream) 87 | httpService.startRunning() 88 | print("Service Started") 89 | } 90 | } 91 | 92 | let service = Service() 93 | 94 | if (CommandLine.arguments.count >= 2) { 95 | 96 | if CommandLine.arguments[1] == "start" { 97 | service.startService() 98 | } 99 | 100 | if CommandLine.arguments.count >= 3 { 101 | let minute = (CommandLine.arguments[2] as NSString).floatValue 102 | let date = Date().addingTimeInterval(TimeInterval( minute * 60.0 )) 103 | RunLoop.current.run(until: date) 104 | RunLoop.current.perform(#selector(service.stopService), with: nil, afterDelay: TimeInterval( minute * 60.0 )) 105 | } else { 106 | RunLoop.current.run() 107 | } 108 | } 109 | else { 110 | print("please provide valid arguments") 111 | exit(0) 112 | } 113 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Codec/H264Decoder.swift: -------------------------------------------------------------------------------- 1 | import CoreVideo 2 | import Foundation 3 | import AVFoundation 4 | import VideoToolbox 5 | import CoreFoundation 6 | 7 | protocol VideoDecoderDelegate: class { 8 | func sampleOutput(video sampleBuffer: CMSampleBuffer) 9 | } 10 | 11 | // MARK: - 12 | final class H264Decoder { 13 | #if os(iOS) 14 | static let defaultAttributes: [NSString: AnyObject] = [ 15 | kCVPixelBufferPixelFormatTypeKey: NSNumber(value: kCVPixelFormatType_32BGRA), 16 | kCVPixelBufferIOSurfacePropertiesKey: [:] as AnyObject, 17 | kCVPixelBufferOpenGLESCompatibilityKey: NSNumber(booleanLiteral: true) 18 | ] 19 | #else 20 | static let defaultAttributes: [NSString: AnyObject] = [ 21 | kCVPixelBufferPixelFormatTypeKey: NSNumber(value: kCVPixelFormatType_32BGRA), 22 | kCVPixelBufferIOSurfacePropertiesKey: [:] as AnyObject, 23 | kCVPixelBufferOpenGLCompatibilityKey: NSNumber(booleanLiteral: true) 24 | ] 25 | #endif 26 | 27 | var formatDescription: CMFormatDescription? { 28 | didSet { 29 | if let atoms: [String: AnyObject] = formatDescription?.getExtension(by: "SampleDescriptionExtensionAtoms"), let avcC: Data = atoms["avcC"] as? Data { 30 | let config: AVCConfigurationRecord = AVCConfigurationRecord(data: avcC) 31 | isBaseline = config.AVCProfileIndication == 66 32 | } 33 | invalidateSession = true 34 | } 35 | } 36 | weak var delegate: VideoDecoderDelegate? 37 | 38 | private var isBaseline: Bool = true 39 | private var buffers: [CMSampleBuffer] = [] 40 | private var attributes: [NSString: AnyObject] { 41 | return H264Decoder.defaultAttributes 42 | } 43 | private var minimumGroupOfPictures: Int = 12 44 | private(set) var status: OSStatus = noErr { 45 | didSet { 46 | if status != noErr { 47 | print("\(self.status)") 48 | } 49 | } 50 | } 51 | private var invalidateSession: Bool = true 52 | private var callback: VTDecompressionOutputCallback = {( 53 | decompressionOutputRefCon: UnsafeMutableRawPointer?, 54 | sourceFrameRefCon: UnsafeMutableRawPointer?, 55 | status: OSStatus, 56 | infoFlags: VTDecodeInfoFlags, 57 | imageBuffer: CVBuffer?, 58 | presentationTimeStamp: CMTime, 59 | duration: CMTime) in 60 | let decoder: H264Decoder = Unmanaged.fromOpaque(decompressionOutputRefCon!).takeUnretainedValue() 61 | decoder.didOutputForSession(status, infoFlags: infoFlags, imageBuffer: imageBuffer, presentationTimeStamp: presentationTimeStamp, duration: duration) 62 | } 63 | 64 | private var _session: VTDecompressionSession? 65 | private var session: VTDecompressionSession! { 66 | get { 67 | if _session == nil { 68 | guard let formatDescription: CMFormatDescription = formatDescription else { 69 | return nil 70 | } 71 | var record: VTDecompressionOutputCallbackRecord = VTDecompressionOutputCallbackRecord( 72 | decompressionOutputCallback: callback, 73 | decompressionOutputRefCon: Unmanaged.passUnretained(self).toOpaque() 74 | ) 75 | guard VTDecompressionSessionCreate( 76 | kCFAllocatorDefault, 77 | formatDescription, 78 | nil, 79 | attributes as CFDictionary?, 80 | &record, 81 | &_session ) == noErr else { 82 | return nil 83 | } 84 | invalidateSession = false 85 | } 86 | return _session! 87 | } 88 | set { 89 | if let session: VTDecompressionSession = _session { 90 | VTDecompressionSessionInvalidate(session) 91 | } 92 | _session = newValue 93 | } 94 | } 95 | 96 | func decodeSampleBuffer(_ sampleBuffer: CMSampleBuffer) -> OSStatus { 97 | guard let session: VTDecompressionSession = session else { 98 | return kVTInvalidSessionErr 99 | } 100 | var flagsOut: VTDecodeInfoFlags = VTDecodeInfoFlags() 101 | let decodeFlags: VTDecodeFrameFlags = VTDecodeFrameFlags(rawValue: 102 | VTDecodeFrameFlags._EnableAsynchronousDecompression.rawValue | 103 | VTDecodeFrameFlags._EnableTemporalProcessing.rawValue 104 | ) 105 | return VTDecompressionSessionDecodeFrame(session, sampleBuffer, decodeFlags, nil, &flagsOut) 106 | } 107 | 108 | func didOutputForSession(_ status: OSStatus, infoFlags: VTDecodeInfoFlags, imageBuffer: CVImageBuffer?, presentationTimeStamp: CMTime, duration: CMTime) { 109 | guard let imageBuffer: CVImageBuffer = imageBuffer, status == noErr else { 110 | return 111 | } 112 | 113 | var timingInfo: CMSampleTimingInfo = CMSampleTimingInfo( 114 | duration: duration, 115 | presentationTimeStamp: presentationTimeStamp, 116 | decodeTimeStamp: kCMTimeInvalid 117 | ) 118 | 119 | var videoFormatDescription: CMVideoFormatDescription? 120 | self.status = CMVideoFormatDescriptionCreateForImageBuffer( 121 | kCFAllocatorDefault, 122 | imageBuffer, 123 | &videoFormatDescription 124 | ) 125 | 126 | var sampleBuffer: CMSampleBuffer? 127 | self.status = CMSampleBufferCreateForImageBuffer( 128 | kCFAllocatorDefault, 129 | imageBuffer, 130 | true, 131 | nil, 132 | nil, 133 | videoFormatDescription!, 134 | &timingInfo, 135 | &sampleBuffer 136 | ) 137 | 138 | guard let buffer: CMSampleBuffer = sampleBuffer else { 139 | return 140 | } 141 | 142 | if isBaseline { 143 | delegate?.sampleOutput(video: buffer) 144 | } else { 145 | buffers.append(buffer) 146 | buffers.sort(by: { (lhs: CMSampleBuffer, rhs: CMSampleBuffer) -> Bool in 147 | return lhs.presentationTimeStamp < rhs.presentationTimeStamp 148 | }) 149 | if minimumGroupOfPictures <= buffers.count { 150 | delegate?.sampleOutput(video: buffers.removeFirst()) 151 | } 152 | } 153 | } 154 | 155 | func clear() { 156 | buffers.removeAll() 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Core/Constant.swift: -------------------------------------------------------------------------------- 1 | //import Logboard 2 | 3 | //let logger: Logboard = Logboard.with(HaishinKitIdentifier) 4 | 5 | public enum CMSampleBufferType: String { 6 | case video 7 | case audio 8 | } 9 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Core/Protocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol DataConvertible { 4 | var data: Data { get set } 5 | } 6 | 7 | // MARK: - 8 | protocol Running: class { 9 | var running: Bool { get } 10 | func startRunning() 11 | func stopRunning() 12 | } 13 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Extension/CMAudioFormatDescription+Extension.swift: -------------------------------------------------------------------------------- 1 | import CoreMedia 2 | 3 | extension CMAudioFormatDescription { 4 | var streamBasicDescription: UnsafePointer? { 5 | return CMAudioFormatDescriptionGetStreamBasicDescription(self) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Extension/CMBlockBuffer+Extension.swift: -------------------------------------------------------------------------------- 1 | import CoreMedia 2 | 3 | extension CMBlockBuffer { 4 | var dataLength: Int { 5 | return CMBlockBufferGetDataLength(self) 6 | } 7 | 8 | var data: Data? { 9 | var length: Int = 0 10 | var buffer: UnsafeMutablePointer? = nil 11 | guard CMBlockBufferGetDataPointer(self, 0, nil, &length, &buffer) == noErr else { 12 | return nil 13 | } 14 | return Data(bytes: buffer!, count: length) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Extension/CMFormatDescription+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreMedia 3 | 4 | extension CMFormatDescription { 5 | var extensions: [String: AnyObject]? { 6 | return CMFormatDescriptionGetExtensions(self) as? [String: AnyObject] 7 | } 8 | 9 | func getExtension(by key: String) -> [String: AnyObject]? { 10 | return CMFormatDescriptionGetExtension(self, key as CFString) as? [String: AnyObject] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Extension/CMSampleBuffer+Extension.swift: -------------------------------------------------------------------------------- 1 | import CoreMedia 2 | 3 | extension CMSampleBuffer { 4 | var dependsOnOthers: Bool { 5 | guard 6 | let attachments: CFArray = CMSampleBufferGetSampleAttachmentsArray(self, false) else { 7 | return false 8 | } 9 | let attachment: [NSObject: AnyObject] = unsafeBitCast(CFArrayGetValueAtIndex(attachments, 0), to: CFDictionary.self) as [NSObject: AnyObject] 10 | return attachment["DependsOnOthers" as NSObject] as! Bool 11 | } 12 | var dataBuffer: CMBlockBuffer? { 13 | get { 14 | return CMSampleBufferGetDataBuffer(self) 15 | } 16 | set { 17 | guard let dataBuffer: CMBlockBuffer = newValue else { 18 | return 19 | } 20 | CMSampleBufferSetDataBuffer(self, dataBuffer) 21 | } 22 | } 23 | var imageBuffer: CVImageBuffer? { 24 | return CMSampleBufferGetImageBuffer(self) 25 | } 26 | var numSamples: CMItemCount { 27 | return CMSampleBufferGetNumSamples(self) 28 | } 29 | var duration: CMTime { 30 | return CMSampleBufferGetDuration(self) 31 | } 32 | var formatDescription: CMFormatDescription? { 33 | return CMSampleBufferGetFormatDescription(self) 34 | } 35 | var decodeTimeStamp: CMTime { 36 | return CMSampleBufferGetDecodeTimeStamp(self) 37 | } 38 | var presentationTimeStamp: CMTime { 39 | return CMSampleBufferGetPresentationTimeStamp(self) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Extension/CMSampleTimingInfo+Extension.swift: -------------------------------------------------------------------------------- 1 | import CoreMedia 2 | 3 | extension CMSampleTimingInfo { 4 | init(sampleBuffer: CMSampleBuffer) { 5 | duration = sampleBuffer.duration 6 | decodeTimeStamp = sampleBuffer.decodeTimeStamp 7 | presentationTimeStamp = sampleBuffer.presentationTimeStamp 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Extension/CMVideoFormatDescription+Extension.swift: -------------------------------------------------------------------------------- 1 | import CoreMedia 2 | import CoreImage 3 | 4 | extension CMVideoFormatDescription { 5 | var dimensions: CMVideoDimensions { 6 | return CMVideoFormatDescriptionGetDimensions(self) 7 | } 8 | 9 | static func create(withPixelBuffer: CVPixelBuffer) -> CMVideoFormatDescription? { 10 | var formatDescription: CMFormatDescription? 11 | let status: OSStatus = CMVideoFormatDescriptionCreate( 12 | kCFAllocatorDefault, 13 | kCMVideoCodecType_422YpCbCr8, 14 | Int32(withPixelBuffer.width), 15 | Int32(withPixelBuffer.height), 16 | nil, 17 | &formatDescription 18 | ) 19 | guard status == noErr else { 20 | print("\(status)") 21 | return nil 22 | } 23 | return formatDescription 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Extension/CVPixelBuffer+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreVideo 3 | import CoreImage 4 | 5 | extension CVPixelBuffer { 6 | static func create(_ image: CIImage) -> CVPixelBuffer? { 7 | var buffer: CVPixelBuffer? 8 | CVPixelBufferCreate( 9 | kCFAllocatorDefault, 10 | Int(image.extent.width), 11 | Int(image.extent.height), 12 | kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, 13 | nil, 14 | &buffer 15 | ) 16 | return buffer 17 | } 18 | var width: Int { 19 | return CVPixelBufferGetWidth(self) 20 | } 21 | var height: Int { 22 | return CVPixelBufferGetHeight(self) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Extension/Data+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Data { 4 | var bytes: [UInt8] { 5 | return withUnsafeBytes { 6 | [UInt8](UnsafeBufferPointer(start: $0, count: count)) 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Extension/ExpressibleByIntegerLiteral+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension ExpressibleByIntegerLiteral { 4 | var data: Data { 5 | var value: Self = self 6 | let s: Int = MemoryLayout<`Self`>.size 7 | return withUnsafeMutablePointer(to: &value) { 8 | $0.withMemoryRebound(to: UInt8.self, capacity: s) { 9 | Data(UnsafeBufferPointer(start: $0, count: s)) 10 | } 11 | } 12 | } 13 | 14 | init(data: Data) { 15 | let diff: Int = MemoryLayout.size - data.count 16 | if 0 < diff { 17 | var buffer: Data = Data(repeating: 0, count: diff) 18 | buffer.append(data) 19 | self = buffer.withUnsafeBytes { $0.pointee } 20 | return 21 | } 22 | self = data.withUnsafeBytes { $0.pointee } 23 | } 24 | 25 | init(data: MutableRangeReplaceableRandomAccessSlice) { 26 | self.init(data: Data(data)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Extension/Mirror+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Mirror { 4 | var description: String { 5 | var data: [String] = [] 6 | if let superclassMirror: Mirror = superclassMirror { 7 | for child in superclassMirror.children { 8 | guard let label: String = child.label else { 9 | continue 10 | } 11 | data.append("\(label): \(child.value)") 12 | } 13 | } 14 | for child in children { 15 | guard let label: String = child.label else { 16 | continue 17 | } 18 | data.append("\(label): \(child.value)") 19 | } 20 | return "\(subjectType){\(data.joined(separator: ","))}" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Extension/URL+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension URL { 4 | var absoluteWithoutAuthenticationString: String { 5 | var target: String = "" 6 | if let user: String = user { 7 | target += user 8 | } 9 | if let password: String = password { 10 | target += ": " + password 11 | } 12 | if target != "" { 13 | target += "@" 14 | } 15 | return absoluteString.replacingOccurrences(of: target, with: "") 16 | } 17 | 18 | var absoluteWithoutQueryString: String { 19 | guard let query: String = self.query else { 20 | return self.absoluteString 21 | } 22 | return absoluteString.replacingOccurrences(of: "?" + query, with: "") 23 | } 24 | 25 | func dictionaryFromQuery() -> [String: String] { 26 | var result: [String: String] = [: ] 27 | guard 28 | let comonents: URLComponents = URLComponents(string: absoluteString), 29 | let queryItems = comonents.queryItems else { 30 | return result 31 | } 32 | for item in queryItems { 33 | if let value: String = item.value { 34 | result[item.name] = value 35 | } 36 | } 37 | return result 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Extension/VTSession+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import VideoToolbox 3 | 4 | extension VTCompressionSession { 5 | func copySupportedPropertyDictionary() -> [AnyHashable: Any] { 6 | var support: CFDictionary? = nil 7 | guard VTSessionCopySupportedPropertyDictionary(self, &support) == noErr else { 8 | return [:] 9 | } 10 | guard let result: [AnyHashable: Any] = support as? [AnyHashable: Any] else { 11 | return [:] 12 | } 13 | return result 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/HTTP/HTTPRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol HTTPRequestCompatible: CustomStringConvertible { 4 | var uri: String { get set } 5 | var method: String { get set } 6 | var version: String { get set } 7 | var headerFields: [String: String] { get set } 8 | var body: Data? { get set } 9 | } 10 | 11 | extension HTTPRequestCompatible { 12 | // MARK: CustomStringConvertible 13 | public var description: String { 14 | return Mirror(reflecting: self).description 15 | } 16 | } 17 | 18 | extension HTTPRequestCompatible { 19 | var data: Data { 20 | get { 21 | var data: Data = Data() 22 | var lines: [String] = ["\(method) \(uri) \(version)"] 23 | for (field, value) in headerFields { 24 | lines.append("\(field): \(value)") 25 | } 26 | data.append(contentsOf: lines.joined(separator: "\r\n").utf8) 27 | return data 28 | } 29 | set { 30 | var lines: [String] = [] 31 | let bytes: [Data.SubSequence] = newValue.split(separator: HTTPRequest.separator) 32 | for i in 0..= 3 else { 45 | return 46 | } 47 | 48 | method = first[0] 49 | uri = first[1] 50 | version = first[2] 51 | for i in 1.. (MIME, String)? { 30 | let url: URL = URL(fileURLWithPath: resourceName) 31 | guard let name: String = name, 2 <= url.pathComponents.count && url.pathComponents[1] == name else { 32 | return nil 33 | } 34 | let fileName: String = url.pathComponents.last! 35 | switch true { 36 | case fileName == "playlist.m3u8": 37 | return (.ApplicationXMpegURL, tsWriter.playlist) 38 | case fileName.contains(".ts"): 39 | if let mediaFile: String = tsWriter.getFilePath(fileName) { 40 | return (.VideoMP2T, mediaFile) 41 | } 42 | return nil 43 | default: 44 | return nil 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/HTTP/M3U.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | - seealso: https: //tools.ietf.org/html/draft-pantos-http-live-streaming-19 5 | */ 6 | struct M3U { 7 | static let header: String = "#EXTM3U" 8 | static let defaultVersion: Int = 3 9 | 10 | var version: Int = M3U.defaultVersion 11 | var mediaList: [M3UMediaInfo] = [] 12 | var mediaSequence: Int = 0 13 | var targetDuration: Double = 5 14 | } 15 | 16 | extension M3U: CustomStringConvertible { 17 | // MARK: CustomStringConvertible 18 | var description: String { 19 | var lines: [String] = [ 20 | "#EXTM3U", 21 | "#EXT-X-VERSION: \(version)", 22 | "#EXT-X-MEDIA-SEQUENCE: \(mediaSequence)", 23 | "#EXT-X-TARGETDURATION: \(Int(targetDuration))" 24 | ] 25 | for info in mediaList { 26 | lines.append("#EXTINF: \(info.duration),") 27 | lines.append(info.url.pathComponents.last!) 28 | } 29 | return lines.joined(separator: "\r\n") 30 | } 31 | } 32 | 33 | // MARK: - 34 | struct M3UMediaInfo { 35 | var url: URL 36 | var duration: Double 37 | } 38 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/ISO/AudioSpecificConfig.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | 4 | /** 5 | The Audio Specific Config is the global header for MPEG-4 Audio 6 | 7 | - seealse: 8 | - http: //wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Audio_Specific_Config 9 | - http: //wiki.multimedia.cx/?title=Understanding_AAC 10 | */ 11 | struct AudioSpecificConfig { 12 | static let ADTSHeaderSize: Int = 7 13 | 14 | var type: AudioObjectType 15 | var frequency: SamplingFrequency 16 | var channel: ChannelConfiguration 17 | var frameLengthFlag: Bool = false 18 | 19 | var bytes: [UInt8] { 20 | var bytes: [UInt8] = [UInt8](repeating: 0, count: 2) 21 | bytes[0] = type.rawValue << 3 | (frequency.rawValue >> 1 & 0x3) 22 | bytes[1] = (frequency.rawValue & 0x1) << 7 | (channel.rawValue & 0xF) << 3 23 | return bytes 24 | } 25 | 26 | init?(bytes: [UInt8]) { 27 | guard let 28 | type: AudioObjectType = AudioObjectType(rawValue: bytes[0] >> 3), 29 | let frequency: SamplingFrequency = SamplingFrequency(rawValue: (bytes[0] & 0b00000111) << 1 | (bytes[1] >> 7)), 30 | let channel: ChannelConfiguration = ChannelConfiguration(rawValue: (bytes[1] & 0b01111000) >> 3) else { 31 | return nil 32 | } 33 | self.type = type 34 | self.frequency = frequency 35 | self.channel = channel 36 | } 37 | 38 | init(type: AudioObjectType, frequency: SamplingFrequency, channel: ChannelConfiguration) { 39 | self.type = type 40 | self.frequency = frequency 41 | self.channel = channel 42 | } 43 | 44 | init(formatDescription: CMFormatDescription) { 45 | let asbd: AudioStreamBasicDescription = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription)!.pointee 46 | type = AudioObjectType(objectID: MPEG4ObjectID(rawValue: Int(asbd.mFormatFlags))!) 47 | frequency = SamplingFrequency(sampleRate: asbd.mSampleRate) 48 | channel = ChannelConfiguration(rawValue: UInt8(asbd.mChannelsPerFrame))! 49 | } 50 | 51 | func adts(_ length: Int) -> [UInt8] { 52 | let size: Int = 7 53 | let fullSize: Int = size + length 54 | var adts: [UInt8] = [UInt8](repeating: 0x00, count: size) 55 | adts[0] = 0xFF 56 | adts[1] = 0xF9 57 | adts[2] = (type.rawValue - 1) << 6 | (frequency.rawValue << 2) | (channel.rawValue >> 2) 58 | adts[3] = (channel.rawValue & 3) << 6 | UInt8(fullSize >> 11) 59 | adts[4] = UInt8((fullSize & 0x7FF) >> 3) 60 | adts[5] = ((UInt8(fullSize & 7)) << 5) + 0x1F 61 | adts[6] = 0xFC 62 | return adts 63 | } 64 | 65 | func createAudioStreamBasicDescription() -> AudioStreamBasicDescription { 66 | var asbd: AudioStreamBasicDescription = AudioStreamBasicDescription() 67 | asbd.mSampleRate = frequency.sampleRate 68 | asbd.mFormatID = kAudioFormatMPEG4AAC 69 | asbd.mFormatFlags = UInt32(type.rawValue) 70 | asbd.mBytesPerPacket = 0 71 | asbd.mFramesPerPacket = frameLengthFlag ? 960 : 1024 72 | asbd.mBytesPerFrame = 0 73 | asbd.mChannelsPerFrame = UInt32(channel.rawValue) 74 | asbd.mBitsPerChannel = 0 75 | asbd.mReserved = 0 76 | return asbd 77 | } 78 | } 79 | 80 | extension AudioSpecificConfig: CustomStringConvertible { 81 | // MARK: CustomStringConvertible 82 | var description: String { 83 | return Mirror(reflecting: self).description 84 | } 85 | } 86 | 87 | // MARK: - 88 | enum AudioObjectType: UInt8 { 89 | case unknown = 0 90 | case aacMain = 1 91 | case aaclc = 2 92 | case aacssr = 3 93 | case aacltp = 4 94 | case aacsbr = 5 95 | case aacScalable = 6 96 | case twinqVQ = 7 97 | case celp = 8 98 | case hxvc = 9 99 | 100 | init(objectID: MPEG4ObjectID) { 101 | switch objectID { 102 | case .aac_Main: 103 | self = .aacMain 104 | case .AAC_LC: 105 | self = .aaclc 106 | case .AAC_SSR: 107 | self = .aacssr 108 | case .AAC_LTP: 109 | self = .aacltp 110 | case .AAC_SBR: 111 | self = .aacsbr 112 | case .aac_Scalable: 113 | self = .aacScalable 114 | case .twinVQ: 115 | self = .twinqVQ 116 | case .CELP: 117 | self = .celp 118 | case .HVXC: 119 | self = .hxvc 120 | } 121 | } 122 | } 123 | 124 | // MARK: - 125 | enum SamplingFrequency: UInt8 { 126 | case hz96000 = 0 127 | case hz88200 = 1 128 | case hz64000 = 2 129 | case hz48000 = 3 130 | case hz44100 = 4 131 | case hz32000 = 5 132 | case hz24000 = 6 133 | case hz22050 = 7 134 | case hz16000 = 8 135 | case hz12000 = 9 136 | case hz11025 = 10 137 | case hz8000 = 11 138 | case hz7350 = 12 139 | 140 | var sampleRate: Float64 { 141 | switch self { 142 | case .hz96000: 143 | return 96000 144 | case .hz88200: 145 | return 88200 146 | case .hz64000: 147 | return 64000 148 | case .hz48000: 149 | return 48000 150 | case .hz44100: 151 | return 44100 152 | case .hz32000: 153 | return 32000 154 | case .hz24000: 155 | return 24000 156 | case .hz22050: 157 | return 22050 158 | case .hz16000: 159 | return 16000 160 | case .hz12000: 161 | return 12000 162 | case .hz11025: 163 | return 11025 164 | case .hz8000: 165 | return 8000 166 | case .hz7350: 167 | return 7350 168 | } 169 | } 170 | 171 | init(sampleRate: Float64) { 172 | switch Int(sampleRate) { 173 | case 96000: 174 | self = .hz96000 175 | case 88200: 176 | self = .hz88200 177 | case 64000: 178 | self = .hz64000 179 | case 48000: 180 | self = .hz48000 181 | case 44100: 182 | self = .hz44100 183 | case 32000: 184 | self = .hz32000 185 | case 24000: 186 | self = .hz24000 187 | case 22050: 188 | self = .hz22050 189 | case 16000: 190 | self = .hz16000 191 | case 12000: 192 | self = .hz12000 193 | case 11025: 194 | self = .hz11025 195 | case 8000: 196 | self = .hz8000 197 | case 7350: 198 | self = .hz7350 199 | default: 200 | self = .hz44100 201 | } 202 | } 203 | } 204 | 205 | // MARK: - 206 | enum ChannelConfiguration: UInt8 { 207 | case definedInAOTSpecificConfig = 0 208 | case frontCenter = 1 209 | case frontLeftAndFrontRight = 2 210 | case frontCenterAndFrontLeftAndFrontRight = 3 211 | case frontCenterAndFrontLeftAndFrontRightAndBackCenter = 4 212 | case frontCenterAndFrontLeftAndFrontRightAndBackLeftAndBackRight = 5 213 | case frontCenterAndFrontLeftAndFrontRightAndBackLeftAndBackRightLFE = 6 214 | case frontCenterAndFrontLeftAndFrontRightAndSideLeftAndSideRightAndBackLeftAndBackRightLFE = 7 215 | } 216 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/ISO/H264+AVC.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | import VideoToolbox 4 | 5 | struct AVCFormatStream { 6 | let data: Data 7 | 8 | init(data: Data) { 9 | self.data = data 10 | } 11 | 12 | init?(data: Data?) { 13 | guard let data: Data = data else { 14 | return nil 15 | } 16 | self.init(data: data) 17 | } 18 | 19 | func toByteStream() -> Data { 20 | let buffer: ByteArray = ByteArray(data: data) 21 | var result: Data = Data() 22 | while 0 < buffer.bytesAvailable { 23 | do { 24 | let length: Int = try Int(buffer.readUInt32()) 25 | result.append(contentsOf: [0x00, 0x00, 0x00, 0x01]) 26 | result.append(try buffer.readBytes(length)) 27 | } catch { 28 | print("\(buffer)") 29 | } 30 | } 31 | return result 32 | } 33 | } 34 | 35 | // MARK: - 36 | /* 37 | - seealso: ISO/IEC 14496-15 2010 38 | */ 39 | struct AVCConfigurationRecord { 40 | 41 | static func getData(_ formatDescription: CMFormatDescription?) -> Data? { 42 | guard let formatDescription: CMFormatDescription = formatDescription else { 43 | return nil 44 | } 45 | if let atoms: NSDictionary = CMFormatDescriptionGetExtension(formatDescription, "SampleDescriptionExtensionAtoms" as CFString) as? NSDictionary { 46 | return atoms["avcC"] as? Data 47 | } 48 | return nil 49 | } 50 | 51 | static let reserveLengthSizeMinusOne: UInt8 = 0x3F 52 | static let reserveNumOfSequenceParameterSets: UInt8 = 0xE0 53 | static let reserveChromaFormat: UInt8 = 0xFC 54 | static let reserveBitDepthLumaMinus8: UInt8 = 0xF8 55 | static let reserveBitDepthChromaMinus8 = 0xF8 56 | 57 | var configurationVersion: UInt8 = 1 58 | var AVCProfileIndication: UInt8 = 0 59 | var profileCompatibility: UInt8 = 0 60 | var AVCLevelIndication: UInt8 = 0 61 | var lengthSizeMinusOneWithReserved: UInt8 = 0 62 | var numOfSequenceParameterSetsWithReserved: UInt8 = 0 63 | var sequenceParameterSets: [[UInt8]] = [] 64 | var pictureParameterSets: [[UInt8]] = [] 65 | 66 | var chromaFormatWithReserve: UInt8 = 0 67 | var bitDepthLumaMinus8WithReserve: UInt8 = 0 68 | var bitDepthChromaMinus8WithReserve: UInt8 = 0 69 | var sequenceParameterSetExt: [[UInt8]] = [] 70 | 71 | var naluLength: Int32 { 72 | return Int32((lengthSizeMinusOneWithReserved >> 6) + 1) 73 | } 74 | 75 | init() { 76 | } 77 | 78 | init(data: Data) { 79 | self.data = data 80 | } 81 | 82 | func createFormatDescription(_ formatDescriptionOut: UnsafeMutablePointer) -> OSStatus { 83 | var parameterSetPointers: [UnsafePointer] = [ 84 | UnsafePointer(sequenceParameterSets[0]), 85 | UnsafePointer(pictureParameterSets[0]) 86 | ] 87 | var parameterSetSizes: [Int] = [ 88 | sequenceParameterSets[0].count, 89 | pictureParameterSets[0].count 90 | ] 91 | return CMVideoFormatDescriptionCreateFromH264ParameterSets( 92 | kCFAllocatorDefault, 93 | 2, 94 | ¶meterSetPointers, 95 | ¶meterSetSizes, 96 | naluLength, 97 | formatDescriptionOut 98 | ) 99 | } 100 | } 101 | 102 | extension AVCConfigurationRecord: DataConvertible { 103 | // MARK: DataConvertible 104 | var data: Data { 105 | get { 106 | let buffer: ByteArray = ByteArray() 107 | .writeUInt8(configurationVersion) 108 | .writeUInt8(AVCProfileIndication) 109 | .writeUInt8(profileCompatibility) 110 | .writeUInt8(AVCLevelIndication) 111 | .writeUInt8(lengthSizeMinusOneWithReserved) 112 | .writeUInt8(numOfSequenceParameterSetsWithReserved) 113 | for i in 0.. Void 13 | 14 | weak var delegate: MP4SamplerDelegate? 15 | 16 | private var files: [URL] = [] 17 | private var handlers: [URL: Handler?] = [: ] 18 | private let lockQueue: DispatchQueue = DispatchQueue(label: "com.haishinkit.HaishinKit.MP4Sampler.lock") 19 | private let loopQueue: DispatchQueue = DispatchQueue(label: "com.haishinkit.HaishinKit.MP4Sampler.loop") 20 | private let operations: OperationQueue = OperationQueue() 21 | private(set) var running: Bool = false 22 | 23 | func appendFile(_ file: URL, completionHandler: Handler? = nil) { 24 | lockQueue.async { 25 | self.handlers[file] = completionHandler 26 | self.files.append(file) 27 | } 28 | } 29 | 30 | private func execute(url: URL) { 31 | let reader: MP4Reader = MP4Reader(url: url) 32 | 33 | do { 34 | let _: UInt32 = try reader.load() 35 | } catch { 36 | print("") 37 | return 38 | } 39 | 40 | delegate?.didOpen(reader) 41 | let traks: [MP4Box] = reader.getBoxes(byName: "trak") 42 | for i in 0..> 5 42 | type = NALType(rawValue: byte & 0x31) ?? .unspec 43 | payload = try buffer.readBytes(buffer.bytesAvailable) 44 | } catch { 45 | print("\(buffer)") 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/ISO/ProgramSpecific.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | - seealso: https: //en.wikipedia.org/wiki/Program-specific_information 5 | */ 6 | protocol PSIPointer { 7 | var pointerField: UInt8 { get set } 8 | var pointerFillerBytes: Data { get set } 9 | } 10 | 11 | // MARK: - 12 | protocol PSITableHeader { 13 | var tableID: UInt8 { get set } 14 | var sectionSyntaxIndicator: Bool { get set } 15 | var privateBit: Bool { get set } 16 | var sectionLength: UInt16 { get set } 17 | } 18 | 19 | // MARK: - 20 | protocol PSITableSyntax { 21 | var tableIDExtension: UInt16 { get set } 22 | var versionNumber: UInt8 { get set } 23 | var currentNextIndicator: Bool { get set } 24 | var sectionNumber: UInt8 { get set } 25 | var lastSectionNumber: UInt8 { get set } 26 | var tableData: Data { get set } 27 | var crc32: UInt32 { get set } 28 | } 29 | 30 | // MARK: - 31 | class ProgramSpecific: PSIPointer, PSITableHeader, PSITableSyntax { 32 | static let reservedBits: UInt8 = 0x03 33 | static let defaultTableIDExtension: UInt16 = 1 34 | 35 | let mutex: Mutex = Mutex() 36 | 37 | // MARK: PSIPointer 38 | var pointerField: UInt8 = 0 39 | var pointerFillerBytes: Data = Data() 40 | 41 | // MARK: PSITableHeader 42 | var tableID: UInt8 = 0 43 | var sectionSyntaxIndicator: Bool = false 44 | var privateBit: Bool = false 45 | var sectionLength: UInt16 = 0 46 | 47 | // MARK: PSITableSyntax 48 | var tableIDExtension: UInt16 = ProgramSpecific.defaultTableIDExtension 49 | var versionNumber: UInt8 = 0 50 | var currentNextIndicator: Bool = true 51 | var sectionNumber: UInt8 = 0 52 | var lastSectionNumber: UInt8 = 0 53 | var tableData: Data { 54 | get { 55 | return Data() 56 | } 57 | set { 58 | } 59 | } 60 | var crc32: UInt32 = 0 61 | 62 | init() { 63 | } 64 | 65 | init?(_ data: Data) { 66 | self.data = data 67 | } 68 | 69 | func arrayOfPackets(_ PID: UInt16) -> [TSPacket] { 70 | var packets: [TSPacket] = [] 71 | var packet: TSPacket = TSPacket() 72 | packet.payloadUnitStartIndicator = true 73 | packet.PID = PID 74 | _ = packet.fill(data, useAdaptationField: false) 75 | packets.append(packet) 76 | return packets 77 | } 78 | } 79 | 80 | extension ProgramSpecific: DataConvertible { 81 | var data: Data { 82 | get { 83 | let tableData: Data = self.tableData 84 | sectionLength = UInt16(tableData.count) + 9 85 | sectionSyntaxIndicator = tableData.count != 0 86 | let buffer: ByteArray = ByteArray() 87 | .writeUInt8(tableID) 88 | .writeUInt16( 89 | (sectionSyntaxIndicator ? 0x8000 : 0) | 90 | (privateBit ? 0x4000 : 0) | 91 | UInt16(ProgramSpecific.reservedBits) << 12 | 92 | sectionLength 93 | ) 94 | .writeUInt16(tableIDExtension) 95 | .writeUInt8( 96 | ProgramSpecific.reservedBits << 6 | 97 | versionNumber << 1 | 98 | (currentNextIndicator ? 1 : 0) 99 | ) 100 | .writeUInt8(sectionNumber) 101 | .writeUInt8(lastSectionNumber) 102 | .writeBytes(tableData) 103 | crc32 = CRC32.MPEG2.calculate(buffer.data) 104 | return Data([pointerField] + pointerFillerBytes) + buffer.writeUInt32(crc32).data 105 | } 106 | set { 107 | let buffer: ByteArray = ByteArray(data: newValue) 108 | do { 109 | pointerField = try buffer.readUInt8() 110 | pointerFillerBytes = try buffer.readBytes(Int(pointerField)) 111 | tableID = try buffer.readUInt8() 112 | var bytes: Data = try buffer.readBytes(2) 113 | sectionSyntaxIndicator = bytes[0] & 0x80 == 0x80 114 | privateBit = bytes[0] & 0x40 == 0x40 115 | sectionLength = UInt16(bytes[0] & 0x03) << 8 | UInt16(bytes[1]) 116 | tableIDExtension = try buffer.readUInt16() 117 | versionNumber = try buffer.readUInt8() 118 | currentNextIndicator = versionNumber & 0x01 == 0x01 119 | versionNumber = (versionNumber & 0b00111110) >> 1 120 | sectionNumber = try buffer.readUInt8() 121 | lastSectionNumber = try buffer.readUInt8() 122 | tableData = try buffer.readBytes(Int(sectionLength - 9)) 123 | crc32 = try buffer.readUInt32() 124 | } catch { 125 | print("\(buffer)") 126 | } 127 | } 128 | } 129 | } 130 | 131 | extension ProgramSpecific: CustomStringConvertible { 132 | // MARK: CustomStringConvertible 133 | var description: String { 134 | return Mirror(reflecting: self).description 135 | } 136 | } 137 | 138 | // MARK: - 139 | final class ProgramAssociationSpecific: ProgramSpecific { 140 | static let tableID: UInt8 = 0 141 | 142 | var programs: [UInt16: UInt16] = [: ] 143 | 144 | override var tableData: Data { 145 | get { 146 | let buffer: ByteArray = ByteArray() 147 | for (number, programMapPID) in programs { 148 | buffer.writeUInt16(number).writeUInt16(programMapPID | 0xe000) 149 | } 150 | return buffer.data 151 | } 152 | set { 153 | let buffer: ByteArray = ByteArray(data: newValue) 154 | do { 155 | for _ in 0.. Bool in 190 | return lhs.elementaryPID < rhs.elementaryPID 191 | } 192 | for essd in elementaryStreamSpecificData { 193 | bytes.append(essd.data) 194 | } 195 | return ByteArray() 196 | .writeUInt16(PCRPID | 0xe000) 197 | .writeUInt16(programInfoLength | 0xf000) 198 | .writeBytes(bytes) 199 | .data 200 | } 201 | set { 202 | mutex.lock() 203 | defer { mutex.unlock() } 204 | let buffer: ByteArray = ByteArray(data: newValue) 205 | do { 206 | PCRPID = try buffer.readUInt16() & 0x1fff 207 | programInfoLength = try buffer.readUInt16() & 0x03ff 208 | buffer.position += Int(programInfoLength) 209 | var position: Int = 0 210 | while 0 < buffer.bytesAvailable { 211 | position = buffer.position 212 | guard let data: ElementaryStreamSpecificData = ElementaryStreamSpecificData(try buffer.readBytes(buffer.bytesAvailable)) else { 213 | break 214 | } 215 | buffer.position = position + ElementaryStreamSpecificData.fixedHeaderSize + Int(data.ESInfoLength) 216 | elementaryStreamSpecificData.append(data) 217 | } 218 | } catch { 219 | print("\(buffer)") 220 | } 221 | } 222 | } 223 | } 224 | 225 | // MARK: - 226 | enum ElementaryStreamType: UInt8 { 227 | case mpeg1Video = 0x01 228 | case mpeg2Video = 0x02 229 | case mpeg1Audio = 0x03 230 | case mpeg2Audio = 0x04 231 | case mpeg2TabledData = 0x05 232 | case mpeg2PacketizedData = 0x06 233 | 234 | case adtsaac = 0x0F 235 | case h263 = 0x10 236 | 237 | case h264 = 0x1B 238 | case h265 = 0x24 239 | } 240 | 241 | // MARK: - 242 | struct ElementaryStreamSpecificData { 243 | static let fixedHeaderSize: Int = 5 244 | 245 | var streamType: UInt8 = 0 246 | var elementaryPID: UInt16 = 0 247 | var ESInfoLength: UInt16 = 0 248 | var ESDescriptors: Data = Data() 249 | 250 | init() { 251 | } 252 | 253 | init?(_ data: Data) { 254 | self.data = data 255 | } 256 | } 257 | 258 | extension ElementaryStreamSpecificData: DataConvertible { 259 | // MARK: BytesConvertible 260 | var data: Data { 261 | get { 262 | return ByteArray() 263 | .writeUInt8(streamType) 264 | .writeUInt16(elementaryPID | 0xe000) 265 | .writeUInt16(ESInfoLength | 0xf000) 266 | .writeBytes(ESDescriptors) 267 | .data 268 | } 269 | set { 270 | let buffer: ByteArray = ByteArray(data: newValue) 271 | do { 272 | streamType = try buffer.readUInt8() 273 | elementaryPID = try buffer.readUInt16() & 0x0fff 274 | ESInfoLength = try buffer.readUInt16() & 0x01ff 275 | ESDescriptors = try buffer.readBytes(Int(ESInfoLength)) 276 | } catch { 277 | print("\(buffer)") 278 | } 279 | } 280 | } 281 | } 282 | 283 | extension ElementaryStreamSpecificData: CustomStringConvertible { 284 | // MARK: CustomStringConvertible 285 | var description: String { 286 | return Mirror(reflecting: self).description 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/ISO/TSReader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol TSReaderDelegate: class { 4 | func didReadPacketizedElementaryStream(_ data: ElementaryStreamSpecificData, PES: PacketizedElementaryStream) 5 | } 6 | 7 | // MARK: - 8 | class TSReader { 9 | weak var delegate: TSReaderDelegate? 10 | 11 | private(set) var PAT: ProgramAssociationSpecific? { 12 | didSet { 13 | guard let PAT: ProgramAssociationSpecific = PAT else { 14 | return 15 | } 16 | for (channel, PID) in PAT.programs { 17 | dictionaryForPrograms[PID] = channel 18 | } 19 | } 20 | } 21 | private(set) var PMT: [UInt16: ProgramMapSpecific] = [: ] { 22 | didSet { 23 | for (_, pmt) in PMT { 24 | for data in pmt.elementaryStreamSpecificData { 25 | dictionaryForESSpecData[data.elementaryPID] = data 26 | } 27 | } 28 | } 29 | } 30 | private(set) var numberOfPackets: Int = 0 31 | 32 | private var eof: UInt64 = 0 33 | private var cursor: Int = 0 34 | private var fileHandle: FileHandle? 35 | private var dictionaryForPrograms: [UInt16: UInt16] = [: ] 36 | private var dictionaryForESSpecData: [UInt16: ElementaryStreamSpecificData] = [: ] 37 | private var packetizedElementaryStreams: [UInt16: PacketizedElementaryStream] = [: ] 38 | 39 | init(url: URL) throws { 40 | fileHandle = try FileHandle(forReadingFrom: url) 41 | eof = fileHandle!.seekToEndOfFile() 42 | } 43 | 44 | func read() { 45 | while let packet: TSPacket = next() { 46 | numberOfPackets += 1 47 | if packet.PID == 0x0000 { 48 | PAT = ProgramAssociationSpecific(packet.payload) 49 | continue 50 | } 51 | if let channel: UInt16 = dictionaryForPrograms[packet.PID] { 52 | PMT[channel] = ProgramMapSpecific(packet.payload) 53 | continue 54 | } 55 | if let data: ElementaryStreamSpecificData = dictionaryForESSpecData[packet.PID] { 56 | readPacketizedElementaryStream(data, packet: packet) 57 | } 58 | } 59 | } 60 | 61 | func readPacketizedElementaryStream(_ data: ElementaryStreamSpecificData, packet: TSPacket) { 62 | if packet.payloadUnitStartIndicator { 63 | if let PES: PacketizedElementaryStream = packetizedElementaryStreams[packet.PID] { 64 | delegate?.didReadPacketizedElementaryStream(data, PES: PES) 65 | } 66 | packetizedElementaryStreams[packet.PID] = PacketizedElementaryStream(packet.payload) 67 | return 68 | } 69 | let _: Int? = packetizedElementaryStreams[packet.PID]?.append(packet.payload) 70 | } 71 | 72 | func close() { 73 | fileHandle?.closeFile() 74 | } 75 | } 76 | 77 | extension TSReader: IteratorProtocol { 78 | // MARK: IteratorProtocol 79 | func next() -> TSPacket? { 80 | guard let fileHandle = fileHandle, UInt64(cursor * TSPacket.size) < eof else { 81 | return nil 82 | } 83 | defer { 84 | cursor += 1 85 | } 86 | fileHandle.seek(toFileOffset: UInt64(cursor * TSPacket.size)) 87 | return TSPacket(data: fileHandle.readData(ofLength: TSPacket.size)) 88 | } 89 | } 90 | 91 | extension TSReader: CustomStringConvertible { 92 | // MARK: CustomStringConvertible 93 | var description: String { 94 | return Mirror(reflecting: self).description 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/ISO/TSWriter.swift: -------------------------------------------------------------------------------- 1 | import CoreMedia 2 | import Foundation 3 | 4 | class TSWriter { 5 | static let defaultPATPID: UInt16 = 0 6 | static let defaultPMTPID: UInt16 = 4095 7 | static let defaultVideoPID: UInt16 = 256 8 | static let defaultAudioPID: UInt16 = 257 9 | static let defaultSegmentCount: Int = 3 10 | static let defaultSegmentMaxCount: Int = 12 11 | static let defaultSegmentDuration: Double = 2 12 | 13 | var playlist: String { 14 | var m3u8: M3U = M3U() 15 | m3u8.targetDuration = segmentDuration 16 | if sequence <= TSWriter.defaultSegmentMaxCount { 17 | m3u8.mediaSequence = 0 18 | m3u8.mediaList = files 19 | return m3u8.description 20 | } 21 | let startIndex = max(0, files.count - TSWriter.defaultSegmentCount) 22 | m3u8.mediaSequence = sequence - TSWriter.defaultSegmentMaxCount 23 | m3u8.mediaList = Array(files[startIndex.. String? { 50 | for info in files { 51 | if info.url.absoluteString.contains(fileName) { 52 | return info.url.path 53 | } 54 | } 55 | return nil 56 | } 57 | 58 | func writeSampleBuffer(_ PID: UInt16, streamID: UInt8, sampleBuffer: CMSampleBuffer) { 59 | let presentationTimeStamp: CMTime = sampleBuffer.presentationTimeStamp 60 | if timestamps[PID] == nil { 61 | timestamps[PID] = presentationTimeStamp 62 | if PCRPID == PID { 63 | PCRTimestamp = presentationTimeStamp 64 | } 65 | } 66 | 67 | let config: Any? = streamID == 192 ? audioConfig : videoConfig 68 | guard var PES: PacketizedElementaryStream = PacketizedElementaryStream.create( 69 | sampleBuffer, timestamp: timestamps[PID]!, config: config 70 | ) else { 71 | return 72 | } 73 | 74 | PES.streamID = streamID 75 | 76 | var decodeTimeStamp: CMTime = sampleBuffer.decodeTimeStamp 77 | if decodeTimeStamp == kCMTimeInvalid { 78 | decodeTimeStamp = presentationTimeStamp 79 | } 80 | 81 | var packets: [TSPacket] = split(PID, PES: PES, timestamp: decodeTimeStamp) 82 | let _: Bool = rotateFileHandle(decodeTimeStamp) 83 | 84 | if streamID == 192 { 85 | packets[0].adaptationField?.randomAccessIndicator = true 86 | } else { 87 | packets[0].adaptationField?.randomAccessIndicator = !sampleBuffer.dependsOnOthers 88 | } 89 | 90 | var bytes: Data = Data() 91 | for var packet in packets { 92 | packet.continuityCounter = continuityCounters[PID]! 93 | continuityCounters[PID] = (continuityCounters[PID]! + 1) & 0x0f 94 | bytes.append(packet.data) 95 | } 96 | 97 | self.currentFileHandle?.write(bytes) 98 | 99 | // nstry({ 100 | // self.currentFileHandle?.write(bytes) 101 | // }, { exception in 102 | // self.currentFileHandle?.write(bytes) 103 | // print("\(exception)") 104 | // }) 105 | } 106 | 107 | func split(_ PID: UInt16, PES: PacketizedElementaryStream, timestamp: CMTime) -> [TSPacket] { 108 | var PCR: UInt64? 109 | let duration: Double = timestamp.seconds - PCRTimestamp.seconds 110 | if PCRPID == PID && 0.02 <= duration { 111 | PCR = UInt64((timestamp.seconds - timestamps[PID]!.seconds) * TSTimestamp.resolution) 112 | PCRTimestamp = timestamp 113 | } 114 | var packets: [TSPacket] = [] 115 | for packet in PES.arrayOfPackets(PID, PCR: PCR) { 116 | packets.append(packet) 117 | } 118 | return packets 119 | } 120 | 121 | func rotateFileHandle(_ timestamp: CMTime) -> Bool { 122 | let duration: Double = timestamp.seconds - rotatedTimestamp.seconds 123 | if duration <= segmentDuration { 124 | return false 125 | } 126 | 127 | let fileManager: FileManager = FileManager.default 128 | 129 | #if os(OSX) 130 | let bundleIdentifier: String? = Bundle.main.bundleIdentifier 131 | let temp: String = bundleIdentifier == nil ? NSTemporaryDirectory() : NSTemporaryDirectory() + bundleIdentifier! + "/" 132 | #else 133 | let temp: String = NSTemporaryDirectory() 134 | #endif 135 | 136 | if !fileManager.fileExists(atPath: temp) { 137 | do { 138 | try fileManager.createDirectory(atPath: temp, withIntermediateDirectories: false, attributes: nil) 139 | } catch let error as NSError { 140 | print("\(error)") 141 | } 142 | } 143 | 144 | let filename: String = Int(timestamp.seconds).description + ".ts" 145 | let url: URL = URL(fileURLWithPath: temp + filename) 146 | 147 | if let currentFileURL: URL = currentFileURL { 148 | files.append(M3UMediaInfo(url: currentFileURL, duration: duration)) 149 | sequence += 1 150 | } 151 | 152 | fileManager.createFile(atPath: url.path, contents: nil, attributes: nil) 153 | if TSWriter.defaultSegmentMaxCount <= files.count { 154 | let info: M3UMediaInfo = files.removeFirst() 155 | do { 156 | try fileManager.removeItem(at: info.url as URL) 157 | } catch let e as NSError { 158 | print("\(e)") 159 | } 160 | } 161 | currentFileURL = url 162 | for (pid, _) in continuityCounters { 163 | continuityCounters[pid] = 0 164 | } 165 | 166 | self.currentFileHandle?.synchronizeFile() 167 | 168 | // nstry({ 169 | // self.currentFileHandle?.synchronizeFile() 170 | // }, { exeption in 171 | // print("\(exeption)") 172 | // }) 173 | 174 | currentFileHandle?.closeFile() 175 | currentFileHandle = try? FileHandle(forWritingTo: url) 176 | 177 | PMT.PCRPID = PCRPID 178 | var bytes: Data = Data() 179 | var packets: [TSPacket] = [] 180 | packets.append(contentsOf: PAT.arrayOfPackets(TSWriter.defaultPATPID)) 181 | packets.append(contentsOf: PMT.arrayOfPackets(TSWriter.defaultPMTPID)) 182 | for packet in packets { 183 | bytes.append(packet.data) 184 | } 185 | 186 | self.currentFileHandle?.write(bytes) 187 | 188 | // nstry({ 189 | // self.currentFileHandle?.write(bytes) 190 | // }, { exception in 191 | // print("\(exception)") 192 | // }) 193 | 194 | rotatedTimestamp = timestamp 195 | 196 | return true 197 | } 198 | 199 | func removeFiles() { 200 | let fileManager: FileManager = FileManager.default 201 | for info in files { 202 | do { 203 | try fileManager.removeItem(at: info.url as URL) 204 | } catch let e as NSError { 205 | print("\(e)") 206 | } 207 | } 208 | files.removeAll() 209 | } 210 | } 211 | 212 | extension TSWriter: Running { 213 | // MARK: Running 214 | func startRunning() { 215 | lockQueue.async { 216 | guard self.running else { 217 | return 218 | } 219 | self.running = true 220 | } 221 | } 222 | func stopRunning() { 223 | lockQueue.async { 224 | guard !self.running else { 225 | return 226 | } 227 | self.currentFileURL = nil 228 | self.currentFileHandle = nil 229 | self.removeFiles() 230 | self.running = false 231 | } 232 | } 233 | } 234 | 235 | extension TSWriter: AudioEncoderDelegate { 236 | // MARK: AudioEncoderDelegate 237 | func didSetFormatDescription(audio formatDescription: CMFormatDescription?) { 238 | guard let formatDescription: CMAudioFormatDescription = formatDescription else { 239 | return 240 | } 241 | audioConfig = AudioSpecificConfig(formatDescription: formatDescription) 242 | var data: ElementaryStreamSpecificData = ElementaryStreamSpecificData() 243 | data.streamType = ElementaryStreamType.adtsaac.rawValue 244 | data.elementaryPID = TSWriter.defaultAudioPID 245 | PMT.elementaryStreamSpecificData.append(data) 246 | continuityCounters[TSWriter.defaultAudioPID] = 0 247 | } 248 | 249 | func sampleOutput(audio sampleBuffer: CMSampleBuffer) { 250 | writeSampleBuffer(TSWriter.defaultAudioPID, streamID: 192, sampleBuffer: sampleBuffer) 251 | } 252 | } 253 | 254 | extension TSWriter: VideoEncoderDelegate { 255 | // MARK: VideoEncoderDelegate 256 | func didSetFormatDescription(video formatDescription: CMFormatDescription?) { 257 | guard 258 | let formatDescription: CMFormatDescription = formatDescription, 259 | let avcC: Data = AVCConfigurationRecord.getData(formatDescription) else { 260 | return 261 | } 262 | videoConfig = AVCConfigurationRecord(data: avcC) 263 | var data: ElementaryStreamSpecificData = ElementaryStreamSpecificData() 264 | data.streamType = ElementaryStreamType.h264.rawValue 265 | data.elementaryPID = TSWriter.defaultVideoPID 266 | PMT.elementaryStreamSpecificData.append(data) 267 | continuityCounters[TSWriter.defaultVideoPID] = 0 268 | } 269 | 270 | func sampleOutput(video sampleBuffer: CMSampleBuffer) { 271 | writeSampleBuffer(TSWriter.defaultVideoPID, streamID: 224, sampleBuffer: sampleBuffer) 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Media/AVMixer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | 4 | final public class AVMixer: NSObject { 5 | 6 | static let supportedSettingsKeys: [String] = [ 7 | "fps", 8 | "sessionPreset", 9 | "continuousAutofocus", 10 | "continuousExposure" 11 | ] 12 | 13 | static let defaultFPS: Float64 = 30 14 | static let defaultVideoSettings: [NSString: AnyObject] = [ 15 | kCVPixelBufferPixelFormatTypeKey: NSNumber(value: kCVPixelFormatType_32BGRA) 16 | ] 17 | #if os(iOS) || os(macOS) 18 | static let defaultSessionPreset: String = AVCaptureSession.Preset.medium.rawValue 19 | 20 | @objc var fps: Float64 { 21 | get { return videoIO.fps } 22 | set { videoIO.fps = newValue } 23 | } 24 | 25 | @objc var continuousExposure: Bool { 26 | get { return videoIO.continuousExposure } 27 | set { videoIO.continuousExposure = newValue } 28 | } 29 | 30 | @objc var continuousAutofocus: Bool { 31 | get { return videoIO.continuousAutofocus } 32 | set { videoIO.continuousAutofocus = newValue } 33 | } 34 | 35 | @objc var sessionPreset: String = AVMixer.defaultSessionPreset { 36 | didSet { 37 | guard sessionPreset != oldValue else { 38 | return 39 | } 40 | session.beginConfiguration() 41 | session.sessionPreset = AVCaptureSession.Preset(rawValue: sessionPreset) 42 | session.commitConfiguration() 43 | } 44 | } 45 | 46 | private var _session: AVCaptureSession? 47 | public var session: AVCaptureSession { 48 | get { 49 | if _session == nil { 50 | _session = AVCaptureSession() 51 | _session!.sessionPreset = AVCaptureSession.Preset(rawValue: AVMixer.defaultSessionPreset) 52 | } 53 | return _session! 54 | } 55 | set { 56 | _session = newValue 57 | } 58 | } 59 | #endif 60 | public private(set) lazy var recorder: AVMixerRecorder = AVMixerRecorder() 61 | 62 | deinit { 63 | dispose() 64 | } 65 | 66 | private(set) lazy var audioIO: AudioIOComponent = { 67 | return AudioIOComponent(mixer: self) 68 | }() 69 | 70 | private(set) lazy var videoIO: VideoIOComponent = { 71 | return VideoIOComponent(mixer: self) 72 | }() 73 | 74 | public func dispose() { 75 | #if os(iOS) || os(macOS) 76 | if session.isRunning { 77 | session.stopRunning() 78 | } 79 | #endif 80 | audioIO.dispose() 81 | videoIO.dispose() 82 | } 83 | } 84 | 85 | extension AVMixer { 86 | final func startEncoding(delegate: Any) { 87 | videoIO.encoder.delegate = delegate as? VideoEncoderDelegate 88 | videoIO.encoder.startRunning() 89 | audioIO.encoder.delegate = delegate as? AudioEncoderDelegate 90 | audioIO.encoder.startRunning() 91 | } 92 | final func stopEncoding() { 93 | videoIO.encoder.delegate = nil 94 | videoIO.encoder.stopRunning() 95 | audioIO.encoder.delegate = nil 96 | audioIO.encoder.stopRunning() 97 | } 98 | } 99 | 100 | extension AVMixer { 101 | final func startPlaying() { 102 | audioIO.playback.startRunning() 103 | videoIO.queue.startRunning() 104 | } 105 | final func stopPlaying() { 106 | audioIO.playback.stopRunning() 107 | videoIO.queue.stopRunning() 108 | } 109 | } 110 | 111 | #if os(iOS) || os(macOS) 112 | extension AVMixer: Running { 113 | // MARK: Running 114 | var running: Bool { 115 | return session.isRunning 116 | } 117 | 118 | final func startRunning() { 119 | guard !running else { 120 | return 121 | } 122 | DispatchQueue.global(qos: .userInteractive).async { 123 | self.session.startRunning() 124 | } 125 | } 126 | 127 | final func stopRunning() { 128 | guard running else { 129 | return 130 | } 131 | session.stopRunning() 132 | } 133 | } 134 | #else 135 | extension AVMixer: Running { 136 | // MARK: Running 137 | var running: Bool { 138 | return false 139 | } 140 | final func startRunning() { 141 | } 142 | final func stopRunning() { 143 | } 144 | } 145 | #endif 146 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Media/AudioIOComponent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | 4 | final class AudioIOComponent: IOComponent { 5 | lazy var encoder: AACEncoder = AACEncoder() 6 | lazy var playback: AudioStreamPlayback = AudioStreamPlayback() 7 | let lockQueue: DispatchQueue = DispatchQueue(label: "com.haishinkit.HaishinKit.AudioIOComponent.lock") 8 | 9 | #if os(iOS) || os(macOS) 10 | var input: AVCaptureDeviceInput? { 11 | didSet { 12 | guard let mixer: AVMixer = mixer, oldValue != input else { 13 | return 14 | } 15 | if let oldValue: AVCaptureDeviceInput = oldValue { 16 | mixer.session.removeInput(oldValue) 17 | } 18 | if let input: AVCaptureDeviceInput = input, mixer.session.canAddInput(input) { 19 | mixer.session.addInput(input) 20 | } 21 | } 22 | } 23 | 24 | private var _output: AVCaptureAudioDataOutput? 25 | var output: AVCaptureAudioDataOutput! { 26 | get { 27 | if _output == nil { 28 | _output = AVCaptureAudioDataOutput() 29 | } 30 | return _output 31 | } 32 | set { 33 | if _output == newValue { 34 | return 35 | } 36 | if let output: AVCaptureAudioDataOutput = _output { 37 | output.setSampleBufferDelegate(nil, queue: nil) 38 | mixer?.session.removeOutput(output) 39 | } 40 | _output = newValue 41 | } 42 | } 43 | #endif 44 | 45 | override init(mixer: AVMixer) { 46 | super.init(mixer: mixer) 47 | encoder.lockQueue = lockQueue 48 | } 49 | 50 | func appendSampleBuffer(_ sampleBuffer: CMSampleBuffer) { 51 | mixer?.recorder.appendSampleBuffer(sampleBuffer, mediaType: .audio) 52 | encoder.encodeSampleBuffer(sampleBuffer) 53 | } 54 | 55 | #if os(iOS) || os(macOS) 56 | func attachAudio(_ audio: AVCaptureDevice?, automaticallyConfiguresApplicationAudioSession: Bool) throws { 57 | guard let mixer: AVMixer = mixer else { 58 | return 59 | } 60 | 61 | mixer.session.beginConfiguration() 62 | defer { 63 | mixer.session.commitConfiguration() 64 | } 65 | 66 | output = nil 67 | encoder.invalidate() 68 | 69 | guard let audio: AVCaptureDevice = audio else { 70 | input = nil 71 | return 72 | } 73 | 74 | input = try AVCaptureDeviceInput(device: audio) 75 | #if os(iOS) 76 | mixer.session.automaticallyConfiguresApplicationAudioSession = automaticallyConfiguresApplicationAudioSession 77 | #endif 78 | mixer.session.addOutput(output) 79 | output.setSampleBufferDelegate(self, queue: lockQueue) 80 | } 81 | 82 | func dispose() { 83 | input = nil 84 | output = nil 85 | } 86 | #else 87 | func dispose() { 88 | } 89 | #endif 90 | } 91 | 92 | extension AudioIOComponent: AVCaptureAudioDataOutputSampleBufferDelegate { 93 | // MARK: AVCaptureAudioDataOutputSampleBufferDelegate 94 | func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { 95 | appendSampleBuffer(sampleBuffer) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Media/IOComponent.swift: -------------------------------------------------------------------------------- 1 | import CoreMedia 2 | import Foundation 3 | 4 | class IOComponent: NSObject { 5 | private(set) weak var mixer: AVMixer? 6 | 7 | init(mixer: AVMixer) { 8 | self.mixer = mixer 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Media/ScalingMode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum ScalingMode: String { 4 | case normal = "Normal" 5 | case letterbox = "Letterbox" 6 | case cropSourceToCleanAperture = "CropSourceToCleanAperture" 7 | case trim = "Trim" 8 | } 9 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Media/SoundSpliter.swift: -------------------------------------------------------------------------------- 1 | import CoreMedia 2 | import Foundation 3 | 4 | public protocol SoundSpliterDelegate: class { 5 | func outputSampleBuffer(_ sampleBuffer: CMSampleBuffer) 6 | } 7 | 8 | // MARK: - 9 | public class SoundSpliter: NSObject { 10 | static let defaultSampleSize: Int = 1024 11 | public weak var delegate: SoundSpliterDelegate? 12 | 13 | private let lockQueue: DispatchQueue = DispatchQueue(label: "com.haishinkit.HaishinKit.SoundMixer.lock") 14 | private(set) var status: OSStatus = noErr { 15 | didSet { 16 | if status != 0 { 17 | print("\(self.status)") 18 | } 19 | } 20 | } 21 | private var frameSize: Int = 2048 22 | private var duration: CMTime = kCMTimeZero 23 | private var sampleData: Data = Data() 24 | private var formatDescription: CMFormatDescription? 25 | private var presentationTimeStamp: CMTime = kCMTimeZero 26 | 27 | private var minimumByteSize: Int { 28 | return min(Int.max, sampleData.count) 29 | } 30 | 31 | public func appendSampleBuffer(_ sampleBuffer: CMSampleBuffer) { 32 | if presentationTimeStamp == kCMTimeZero { 33 | duration = CMTime(value: 1, timescale: 44100) 34 | formatDescription = sampleBuffer.formatDescription 35 | presentationTimeStamp = sampleBuffer.presentationTimeStamp 36 | } 37 | 38 | var blockBuffer: CMBlockBuffer? = nil 39 | let audioBufferList: UnsafeMutableAudioBufferListPointer = AudioBufferList.allocate(maximumBuffers: 1) 40 | CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer( 41 | sampleBuffer, 42 | nil, 43 | audioBufferList.unsafeMutablePointer, 44 | AudioBufferList.sizeInBytes(maximumBuffers: 1), 45 | nil, 46 | nil, 47 | 0, 48 | &blockBuffer 49 | ) 50 | 51 | if let mData: UnsafeMutableRawPointer = audioBufferList.unsafePointer.pointee.mBuffers.mData { 52 | sampleData.append( 53 | mData.assumingMemoryBound(to: UInt8.self), 54 | count: Int(audioBufferList.unsafePointer.pointee.mBuffers.mDataByteSize) 55 | ) 56 | } 57 | 58 | lockQueue.async { 59 | self.split() 60 | } 61 | } 62 | 63 | func split() { 64 | guard frameSize < self.minimumByteSize else { 65 | return 66 | } 67 | 68 | let minimumByteSize: Int = self.minimumByteSize 69 | let remain: Int = minimumByteSize % frameSize 70 | let length: Int = minimumByteSize - remain 71 | let sampleData: Data = self.sampleData 72 | 73 | self.sampleData.removeAll() 74 | self.sampleData.append(sampleData.subdata(in: length.. CIImage { 6 | return image 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Net/MIME.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum MIME: String { 4 | case TextPlain = "text/plain" 5 | case TextHtml = "text/html" 6 | case ApplicationXMpegURL = "application/x-mpegURL" 7 | case VndAppleMpegURL = "application/vnd.apple.mpegURL" 8 | case VideoMP2T = "video/MP2T" 9 | } 10 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Net/NetClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @objc protocol NetClientDelegate: class { 4 | @objc optional func client(inputBuffer client: NetClient) 5 | @objc optional func client(didAccepetConnection client: NetClient) 6 | } 7 | 8 | // MARK: - 9 | final public class NetClient: NetSocket { 10 | weak var delegate: NetClientDelegate? 11 | private(set) var service: Foundation.NetService? 12 | 13 | init(service: Foundation.NetService, inputStream: InputStream, outputStream: OutputStream) { 14 | super.init() 15 | self.service = service 16 | self.inputStream = inputStream 17 | self.outputStream = outputStream 18 | } 19 | 20 | func acceptConnection() { 21 | inputQueue.async { 22 | self.initConnection() 23 | self.delegate?.client?(didAccepetConnection: self) 24 | } 25 | } 26 | 27 | override func listen() { 28 | delegate?.client?(inputBuffer: self) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Net/NetService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | open class NetService: NSObject { 4 | 5 | open var txtData: Data? { 6 | return nil 7 | } 8 | 9 | let lockQueue: DispatchQueue = DispatchQueue(label: "com.haishinkit.HaishinKit.NetService.lock") 10 | var networkQueue: DispatchQueue = DispatchQueue(label: "com.haishinkit.HaishinKit.NetService.network") 11 | 12 | private(set) var domain: String 13 | private(set) var name: String 14 | private(set) var port: Int32 15 | private(set) var type: String 16 | private(set) var running: Bool = false 17 | private(set) var clients: [NetClient] = [] 18 | private(set) var service: Foundation.NetService! 19 | private var runloop: RunLoop! 20 | 21 | public init(domain: String, type: String, name: String, port: Int32) { 22 | self.domain = domain 23 | self.name = name 24 | self.port = port 25 | self.type = type 26 | } 27 | 28 | func disconnect(_ client: NetClient) { 29 | lockQueue.sync { 30 | guard let index: Int = clients.index(of: client) else { 31 | return 32 | } 33 | clients.remove(at: index) 34 | client.delegate = nil 35 | client.close(isDisconnected: true) 36 | } 37 | } 38 | 39 | func willStartRunning() { 40 | networkQueue.async { 41 | self.initService() 42 | } 43 | } 44 | 45 | func willStopRunning() { 46 | if let runloop: RunLoop = runloop { 47 | service.remove(from: runloop, forMode: RunLoopMode.defaultRunLoopMode) 48 | CFRunLoopStop(runloop.getCFRunLoop()) 49 | } 50 | service.stop() 51 | service.delegate = nil 52 | service = nil 53 | runloop = nil 54 | } 55 | 56 | private func initService() { 57 | runloop = RunLoop.current 58 | service = Foundation.NetService(domain: domain, type: type, name: name, port: port) 59 | service.delegate = self 60 | service.setTXTRecord(txtData) 61 | service.schedule(in: runloop, forMode: RunLoopMode.defaultRunLoopMode) 62 | if type.contains("._udp") { 63 | service.publish() 64 | } else { 65 | service.publish(options: Foundation.NetService.Options.listenForConnections) 66 | } 67 | runloop.run() 68 | } 69 | } 70 | 71 | extension NetService: NetServiceDelegate { 72 | // MARK: NSNetServiceDelegate 73 | public func netService(_ sender: Foundation.NetService, didAcceptConnectionWith inputStream: InputStream, outputStream: OutputStream) { 74 | lockQueue.sync { 75 | let client: NetClient = NetClient(service: sender, inputStream: inputStream, outputStream: outputStream) 76 | clients.append(client) 77 | client.delegate = self 78 | client.acceptConnection() 79 | } 80 | } 81 | } 82 | 83 | extension NetService: NetClientDelegate { 84 | // MARK: NetClientDelegate 85 | } 86 | 87 | extension NetService: Running { 88 | // MARK: Runnbale 89 | final public func startRunning() { 90 | lockQueue.async { 91 | if self.running { 92 | return 93 | } 94 | self.willStartRunning() 95 | self.running = true 96 | } 97 | } 98 | 99 | final public func stopRunning() { 100 | lockQueue.async { 101 | if !self.running { 102 | return 103 | } 104 | self.willStopRunning() 105 | self.running = false 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Net/NetSocket.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class NetSocket: NSObject { 4 | static public let defaultTimeout: Int64 = 15 // sec 5 | static public let defaultWindowSizeC: Int = Int(UInt16.max) 6 | 7 | public var inputBuffer: Data = Data() 8 | public var timeout: Int64 = NetSocket.defaultTimeout 9 | public internal(set) var connected: Bool = false 10 | public var windowSizeC: Int = NetSocket.defaultWindowSizeC 11 | public var securityLevel: StreamSocketSecurityLevel = .none 12 | public var totalBytesIn: Int64 = 0 13 | public private(set) var totalBytesOut: Int64 = 0 14 | public private(set) var queueBytesOut: Int64 = 0 15 | 16 | var inputStream: InputStream? 17 | var outputStream: OutputStream? 18 | var inputQueue: DispatchQueue = DispatchQueue(label: "com.haishinkit.HaishinKit.NetSocket.input") 19 | 20 | private var buffer: UnsafeMutablePointer? 21 | private var runloop: RunLoop? 22 | private let outputQueue: DispatchQueue = DispatchQueue(label: "com.haishinkit.HaishinKit.NetSocket.output") 23 | private var timeoutHandler: (() -> Void)? 24 | 25 | @discardableResult 26 | final public func doOutput(data: Data, locked: UnsafeMutablePointer? = nil) -> Int { 27 | OSAtomicAdd64(Int64(data.count), &queueBytesOut) 28 | outputQueue.async { 29 | data.withUnsafeBytes { (buffer: UnsafePointer) -> Void in 30 | self.doOutputProcess(buffer, maxLength: data.count) 31 | } 32 | if locked != nil { 33 | OSAtomicAnd32Barrier(0, locked!) 34 | } 35 | } 36 | return data.count 37 | } 38 | 39 | final func doOutputFromURL(_ url: URL, length: Int) { 40 | outputQueue.async { 41 | do { 42 | let fileHandle: FileHandle = try FileHandle(forReadingFrom: url) 43 | defer { 44 | fileHandle.closeFile() 45 | } 46 | let endOfFile: Int = Int(fileHandle.seekToEndOfFile()) 47 | for i in 0..) -> Void in 63 | doOutputProcess(buffer, maxLength: data.count) 64 | } 65 | } 66 | 67 | final func doOutputProcess(_ buffer: UnsafePointer, maxLength: Int) { 68 | guard let outputStream: OutputStream = outputStream else { 69 | return 70 | } 71 | var total: Int = 0 72 | while total < maxLength { 73 | let length: Int = outputStream.write(buffer.advanced(by: total), maxLength: maxLength - total) 74 | if length <= 0 { 75 | break 76 | } 77 | total += length 78 | totalBytesOut += Int64(length) 79 | OSAtomicAdd64(-Int64(length), &queueBytesOut) 80 | } 81 | } 82 | 83 | func close(isDisconnected: Bool) { 84 | outputQueue.async { 85 | guard let runloop: RunLoop = self.runloop else { 86 | return 87 | } 88 | self.deinitConnection(isDisconnected: isDisconnected) 89 | self.runloop = nil 90 | CFRunLoopStop(runloop.getCFRunLoop()) 91 | //logger.trace("isDisconnected: \(isDisconnected)") 92 | } 93 | } 94 | 95 | func listen() { 96 | } 97 | 98 | func initConnection() { 99 | buffer = UnsafeMutablePointer.allocate(capacity: windowSizeC) 100 | buffer?.initialize(to: 0, count: windowSizeC) 101 | 102 | totalBytesIn = 0 103 | totalBytesOut = 0 104 | queueBytesOut = 0 105 | timeoutHandler = didTimeout 106 | inputBuffer.removeAll(keepingCapacity: false) 107 | 108 | guard let inputStream: InputStream = inputStream, let outputStream: OutputStream = outputStream else { 109 | return 110 | } 111 | 112 | runloop = .current 113 | 114 | inputStream.delegate = self 115 | inputStream.schedule(in: runloop!, forMode: .defaultRunLoopMode) 116 | inputStream.setProperty(securityLevel.rawValue, forKey: Stream.PropertyKey.socketSecurityLevelKey) 117 | 118 | outputStream.delegate = self 119 | outputStream.schedule(in: runloop!, forMode: .defaultRunLoopMode) 120 | outputStream.setProperty(securityLevel.rawValue, forKey: Stream.PropertyKey.socketSecurityLevelKey) 121 | 122 | inputStream.open() 123 | outputStream.open() 124 | 125 | if 0 < timeout { 126 | outputQueue.asyncAfter(deadline: DispatchTime.now() + Double(timeout * Int64(NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) { 127 | guard let timeoutHandler: (() -> Void) = self.timeoutHandler else { 128 | return 129 | } 130 | timeoutHandler() 131 | } 132 | } 133 | 134 | runloop?.run() 135 | connected = false 136 | } 137 | 138 | func deinitConnection(isDisconnected: Bool) { 139 | inputStream?.close() 140 | inputStream?.remove(from: runloop!, forMode: .defaultRunLoopMode) 141 | inputStream?.delegate = nil 142 | inputStream = nil 143 | outputStream?.close() 144 | outputStream?.remove(from: runloop!, forMode: .defaultRunLoopMode) 145 | outputStream?.delegate = nil 146 | outputStream = nil 147 | buffer?.deinitialize() 148 | buffer?.deallocate(capacity: windowSizeC) 149 | buffer = nil 150 | } 151 | 152 | func didTimeout() { 153 | } 154 | 155 | private func doInput() { 156 | guard let inputStream: InputStream = inputStream, let buffer: UnsafeMutablePointer = buffer else { 157 | return 158 | } 159 | let length: Int = inputStream.read(buffer, maxLength: windowSizeC) 160 | if 0 < length { 161 | totalBytesIn += Int64(length) 162 | inputBuffer.append(buffer, count: length) 163 | listen() 164 | } 165 | } 166 | } 167 | 168 | extension NetSocket: StreamDelegate { 169 | // MARK: StreamDelegate 170 | public func stream(_ aStream: Stream, handle eventCode: Stream.Event) { 171 | switch eventCode { 172 | // 1 = 1 << 0 173 | case Stream.Event.openCompleted: 174 | guard let inputStream = inputStream, let outputStream = outputStream, 175 | inputStream.streamStatus == .open && outputStream.streamStatus == .open else { 176 | break 177 | } 178 | if aStream == inputStream { 179 | timeoutHandler = nil 180 | connected = true 181 | } 182 | // 2 = 1 << 1 183 | case Stream.Event.hasBytesAvailable: 184 | if aStream == inputStream { 185 | doInput() 186 | } 187 | // 4 = 1 << 2 188 | case Stream.Event.hasSpaceAvailable: 189 | break 190 | // 8 = 1 << 3 191 | case Stream.Event.errorOccurred: 192 | close(isDisconnected: true) 193 | // 16 = 1 << 4 194 | case Stream.Event.endEncountered: 195 | close(isDisconnected: true) 196 | default: 197 | break 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Net/NetStream.swift: -------------------------------------------------------------------------------- 1 | import CoreImage 2 | import Foundation 3 | import AVFoundation 4 | 5 | protocol NetStreamDrawable: class { 6 | #if os(iOS) || os(macOS) 7 | var orientation: AVCaptureVideoOrientation { get set } 8 | var position: AVCaptureDevice.Position { get set } 9 | #endif 10 | 11 | func draw(image: CIImage) 12 | func attachStream(_ stream: NetStream?) 13 | } 14 | 15 | // MARK: - 16 | open class NetStream: NSObject { 17 | public private(set) var mixer: AVMixer = AVMixer() 18 | public let lockQueue: DispatchQueue = DispatchQueue(label: "com.haishinkit.HaishinKit.NetStream.lock") 19 | 20 | deinit { 21 | metadata.removeAll() 22 | NotificationCenter.default.removeObserver(self) 23 | } 24 | 25 | open var metadata: [String: Any?] = [: ] 26 | 27 | open var context: CIContext? { 28 | get { 29 | return mixer.videoIO.context 30 | } 31 | set { 32 | mixer.videoIO.context = newValue 33 | } 34 | } 35 | 36 | #if os(iOS) || os(macOS) 37 | open var torch: Bool { 38 | get { 39 | var torch: Bool = false 40 | lockQueue.sync { 41 | torch = self.mixer.videoIO.torch 42 | } 43 | return torch 44 | } 45 | set { 46 | lockQueue.async { 47 | self.mixer.videoIO.torch = newValue 48 | } 49 | } 50 | } 51 | #endif 52 | 53 | #if os(iOS) 54 | open var syncOrientation: Bool = false { 55 | didSet { 56 | guard syncOrientation != oldValue else { 57 | return 58 | } 59 | if syncOrientation { 60 | NotificationCenter.default.addObserver(self, selector: #selector(NetStream.on(uiDeviceOrientationDidChange: )), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil) 61 | } else { 62 | NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil) 63 | } 64 | } 65 | } 66 | #endif 67 | 68 | open var audioSettings: [String: Any] { 69 | get { 70 | var audioSettings: [String: Any]! 71 | lockQueue.sync { 72 | audioSettings = self.mixer.audioIO.encoder.dictionaryWithValues(forKeys: AACEncoder.supportedSettingsKeys) 73 | } 74 | return audioSettings 75 | } 76 | set { 77 | lockQueue.sync { 78 | self.mixer.audioIO.encoder.setValuesForKeys(newValue) 79 | } 80 | } 81 | } 82 | 83 | open var videoSettings: [String: Any] { 84 | get { 85 | var videoSettings: [String: Any]! 86 | lockQueue.sync { 87 | videoSettings = self.mixer.videoIO.encoder.dictionaryWithValues(forKeys: H264Encoder.supportedSettingsKeys) 88 | } 89 | return videoSettings 90 | } 91 | set { 92 | lockQueue.sync { 93 | self.mixer.videoIO.encoder.setValuesForKeys(newValue) 94 | } 95 | } 96 | } 97 | 98 | open var captureSettings: [String: Any] { 99 | get { 100 | var captureSettings: [String: Any]! 101 | lockQueue.sync { 102 | captureSettings = self.mixer.dictionaryWithValues(forKeys: AVMixer.supportedSettingsKeys) 103 | } 104 | return captureSettings 105 | } 106 | set { 107 | lockQueue.sync { 108 | self.mixer.setValuesForKeys(newValue) 109 | } 110 | } 111 | } 112 | 113 | open var recorderSettings: [AVMediaType: [String: Any]] { 114 | get { 115 | var recorderSettings: [AVMediaType: [String: Any]]! 116 | lockQueue.sync { 117 | recorderSettings = self.mixer.recorder.outputSettings 118 | } 119 | return recorderSettings 120 | } 121 | set { 122 | lockQueue.sync { 123 | self.mixer.recorder.outputSettings = newValue 124 | } 125 | } 126 | } 127 | 128 | #if os(iOS) || os(macOS) 129 | open func attachCamera(_ camera: AVCaptureDevice?, onError: ((_ error: NSError) -> Void)? = nil) { 130 | lockQueue.async { 131 | do { 132 | try self.mixer.videoIO.attachCamera(camera) 133 | } catch let error as NSError { 134 | onError?(error) 135 | } 136 | } 137 | } 138 | 139 | open func attachAudio(_ audio: AVCaptureDevice?, automaticallyConfiguresApplicationAudioSession: Bool = false, onError: ((_ error: NSError) -> Void)? = nil) { 140 | lockQueue.async { 141 | do { 142 | try self.mixer.audioIO.attachAudio(audio, automaticallyConfiguresApplicationAudioSession: automaticallyConfiguresApplicationAudioSession) 143 | } catch let error as NSError { 144 | onError?(error) 145 | } 146 | } 147 | } 148 | 149 | open func setPointOfInterest(_ focus: CGPoint, exposure: CGPoint) { 150 | mixer.videoIO.focusPointOfInterest = focus 151 | mixer.videoIO.exposurePointOfInterest = exposure 152 | } 153 | #endif 154 | 155 | open func appendSampleBuffer(_ sampleBuffer: CMSampleBuffer, withType: CMSampleBufferType, options: [NSObject: AnyObject]? = nil) { 156 | switch withType { 157 | case .audio: 158 | mixer.audioIO.lockQueue.async { 159 | self.mixer.audioIO.appendSampleBuffer(sampleBuffer) 160 | } 161 | case .video: 162 | mixer.videoIO.lockQueue.async { 163 | self.mixer.videoIO.appendSampleBuffer(sampleBuffer) 164 | } 165 | } 166 | } 167 | 168 | open func registerEffect(video effect: VisualEffect) -> Bool { 169 | return mixer.videoIO.registerEffect(effect) 170 | } 171 | 172 | open func unregisterEffect(video effect: VisualEffect) -> Bool { 173 | return mixer.videoIO.unregisterEffect(effect) 174 | } 175 | 176 | open func dispose() { 177 | lockQueue.async { 178 | self.mixer.dispose() 179 | } 180 | } 181 | 182 | #if os(iOS) 183 | @objc private func on(uiDeviceOrientationDidChange: Notification) { 184 | if let orientation: AVCaptureVideoOrientation = DeviceUtil.videoOrientation(by: uiDeviceOrientationDidChange) { 185 | self.orientation = orientation 186 | } 187 | } 188 | #endif 189 | } 190 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/RTMP/ASClass.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public let kASUndefined: ASUndefined = ASUndefined() 4 | public typealias ASObject = [String: Any?] 5 | 6 | public final class ASUndefined: NSObject { 7 | public override var description: String { 8 | return "undefined" 9 | } 10 | fileprivate override init() { 11 | super.init() 12 | } 13 | } 14 | 15 | // MARK: - 16 | public struct ASArray { 17 | private(set) var data: [Any?] 18 | private(set) var dict: [String: Any?] = [: ] 19 | 20 | public var length: Int { 21 | return data.count 22 | } 23 | 24 | public init(count: Int) { 25 | self.data = [Any?](repeating: kASUndefined, count: count) 26 | } 27 | 28 | public init(data: [Any?]) { 29 | self.data = data 30 | } 31 | } 32 | 33 | extension ASArray: ExpressibleByArrayLiteral { 34 | // MARK: ExpressibleByArrayLiteral 35 | public init (arrayLiteral elements: Any?...) { 36 | self = ASArray(data: elements) 37 | } 38 | 39 | public subscript(i: Any) -> Any? { 40 | get { 41 | if let i: Int = i as? Int { 42 | return i < data.count ? data[i] : kASUndefined 43 | } 44 | if let i: String = i as? String { 45 | if let i: Int = Int(i) { 46 | return i < data.count ? data[i] : kASUndefined 47 | } 48 | return dict[i] as Any 49 | } 50 | return nil 51 | } 52 | set { 53 | if let i: Int = i as? Int { 54 | if data.count <= i { 55 | data += [Any?](repeating: kASUndefined, count: i - data.count + 1) 56 | } 57 | data[i] = newValue 58 | } 59 | if let i: String = i as? String { 60 | if let i: Int = Int(i) { 61 | if data.count <= i { 62 | data += [Any?](repeating: kASUndefined, count: i - data.count + 1) 63 | } 64 | data[i] = newValue 65 | return 66 | } 67 | dict[i] = newValue 68 | } 69 | } 70 | } 71 | } 72 | 73 | extension ASArray: CustomStringConvertible { 74 | // MARK: CustomStringConvertible 75 | public var description: String { 76 | return data.description 77 | } 78 | } 79 | 80 | extension ASArray: Equatable { 81 | // MARK: Equatable 82 | } 83 | 84 | public func == (lhs: ASArray, rhs: ASArray) -> Bool { 85 | return (lhs.data.description == rhs.data.description) && (lhs.dict.description == rhs.dict.description) 86 | } 87 | 88 | // MARK: - 89 | /** 90 | ActionScript 1.0 and 2.0 and flash.xml.XMLDocument in ActionScript 3.0 91 | 92 | - seealso: 93 | - 2.17 XML Document Type (amf0-file-format-specification.pdf) 94 | - 3.9 XMLDocument type (amf-file-format-spec.pdf) 95 | */ 96 | public final class ASXMLDocument: NSObject { 97 | public override var description: String { 98 | return data 99 | } 100 | 101 | private var data: String 102 | 103 | public init(data: String) { 104 | self.data = data 105 | } 106 | } 107 | 108 | // MARK: - 109 | /** 110 | ActionScript 3.0 introduces a new XML type. 111 | 112 | - seealso: 3.13 XML type (amf-file-format-spec.pdf) 113 | */ 114 | public final class ASXML: NSObject { 115 | public override var description: String { 116 | return data 117 | } 118 | 119 | private var data: String 120 | 121 | public init(data: String) { 122 | self.data = data 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/RTMP/RTMPChunk.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum RTMPChunkType: UInt8 { 4 | case zero = 0 5 | case one = 1 6 | case two = 2 7 | case three = 3 8 | 9 | var headerSize: Int { 10 | switch self { 11 | case .zero: 12 | return 11 13 | case .one: 14 | return 7 15 | case .two: 16 | return 3 17 | case .three: 18 | return 0 19 | } 20 | } 21 | 22 | func ready(_ data: Data) -> Bool { 23 | return headerSize + RTMPChunk.getStreamIdSize(data[0]) < data.count 24 | } 25 | 26 | func toBasicHeader(_ streamId: UInt16) -> Data { 27 | if streamId <= 63 { 28 | return Data([rawValue << 6 | UInt8(streamId)]) 29 | } 30 | if streamId <= 319 { 31 | return Data([rawValue << 6 | 0b0000000, UInt8(streamId - 64)]) 32 | } 33 | return Data([rawValue << 6 | 0b00000001] + (streamId - 64).bigEndian.data) 34 | } 35 | } 36 | 37 | final class RTMPChunk { 38 | enum StreamID: UInt16 { 39 | case control = 0x02 40 | case command = 0x03 41 | case audio = 0x04 42 | case video = 0x05 43 | } 44 | 45 | static let defaultSize: Int = 128 46 | static let maxTimestamp: UInt32 = 0xFFFFFF 47 | 48 | static func getStreamIdSize(_ byte: UInt8) -> Int { 49 | switch byte & 0b00111111 { 50 | case 0: 51 | return 2 52 | case 1: 53 | return 3 54 | default: 55 | return 1 56 | } 57 | } 58 | 59 | var size: Int = 0 60 | var type: RTMPChunkType = .zero 61 | var streamId: UInt16 = RTMPChunk.StreamID.command.rawValue 62 | 63 | var ready: Bool { 64 | guard let message: RTMPMessage = message else { 65 | return false 66 | } 67 | return message.length == message.payload.count 68 | } 69 | 70 | var headerSize: Int { 71 | if streamId <= 63 { 72 | return 1 + type.headerSize 73 | } 74 | if streamId <= 319 { 75 | return 2 + type.headerSize 76 | } 77 | return 3 + type.headerSize 78 | } 79 | 80 | var basicHeaderSize: Int { 81 | if streamId <= 63 { 82 | return 1 83 | } 84 | if streamId <= 319 { 85 | return 2 86 | } 87 | return 3 88 | } 89 | 90 | var data: Data { 91 | get { 92 | guard let message: RTMPMessage = message else { 93 | return _data 94 | } 95 | 96 | guard _data.isEmpty else { 97 | var data: Data = Data() 98 | data.append(_data) 99 | data.append(message.payload) 100 | return data 101 | } 102 | 103 | _data.append(type.toBasicHeader(streamId)) 104 | 105 | if RTMPChunk.maxTimestamp < message.timestamp { 106 | _data.append(contentsOf: [0xFF, 0xFF, 0xFF]) 107 | } else { 108 | _data.append(contentsOf: message.timestamp.bigEndian.data[1...3]) 109 | } 110 | _data.append(contentsOf: UInt32(message.payload.count).bigEndian.data[1...3]) 111 | _data.append(message.type.rawValue) 112 | 113 | if type == .zero { 114 | _data.append(message.streamId.littleEndian.data) 115 | } 116 | 117 | if RTMPChunk.maxTimestamp < message.timestamp { 118 | _data.append(message.timestamp.bigEndian.data) 119 | } 120 | 121 | var data: Data = Data() 122 | data.append(_data) 123 | data.append(message.payload) 124 | 125 | return data 126 | } 127 | set { 128 | if _data == newValue { 129 | return 130 | } 131 | 132 | var pos: Int = 0 133 | switch newValue[0] & 0b00111111 { 134 | case 0: 135 | pos = 2 136 | streamId = UInt16(newValue[1]) + 64 137 | case 1: 138 | pos = 3 139 | streamId = UInt16(data: newValue[1...2]) + 64 140 | default: 141 | pos = 1 142 | streamId = UInt16(newValue[0] & 0b00111111) 143 | } 144 | 145 | _data.append(newValue[0..> 6), type.ready(data) else { 201 | return nil 202 | } 203 | self.size = size 204 | self.type = type 205 | self.data = data 206 | } 207 | 208 | func append(_ data: Data, size: Int) -> Int { 209 | fragmented = false 210 | 211 | guard let message: RTMPMessage = message else { 212 | return 0 213 | } 214 | 215 | var length: Int = message.length - message.payload.count 216 | 217 | if data.count < length { 218 | length = data.count 219 | } 220 | 221 | let chunkSize: Int = size - (message.payload.count % size) 222 | if chunkSize < length { 223 | length = chunkSize 224 | } 225 | 226 | if 0 < length { 227 | message.payload.append(data[0.. Int { 236 | guard let message: RTMPMessage = message else { 237 | return 0 238 | } 239 | 240 | let buffer: ByteArray = ByteArray(data: data) 241 | buffer.position = basicHeaderSize 242 | 243 | do { 244 | self.message = RTMPMessage.create(message.type.rawValue) 245 | self.message?.streamId = message.streamId 246 | self.message?.timestamp = try buffer.readUInt24() 247 | self.message?.length = message.length 248 | self.message?.payload = Data(try buffer.readBytes(message.length)) 249 | } catch { 250 | print("\(buffer)") 251 | } 252 | 253 | return headerSize + message.length 254 | } 255 | 256 | func split(_ size: Int) -> [Data] { 257 | let data: Data = self.data 258 | message?.length = data.count 259 | guard let message: RTMPMessage = message, size < message.payload.count else { 260 | return [data] 261 | } 262 | let startIndex: Int = size + headerSize 263 | let header: Data = RTMPChunkType.three.toBasicHeader(streamId) 264 | var chunks: [Data] = [data.subdata(in: 0.. Data { 21 | return ByteArray() 22 | .writeBytes(s0s1packet.subdata(in: 1..<5)) 23 | .writeInt32(Int32(Date().timeIntervalSince1970 - timestamp)) 24 | .writeBytes(s0s1packet.subdata(in: 9.. RTMPSharedObject { 87 | let key: String = remotePath + "/" + withName + "?persistence=" + persistence.description 88 | objc_sync_enter(remoteSharedObjects) 89 | if remoteSharedObjects[key] == nil { 90 | remoteSharedObjects[key] = RTMPSharedObject(name: withName, path: remotePath, persistence: persistence) 91 | } 92 | objc_sync_exit(remoteSharedObjects) 93 | return remoteSharedObjects[key]! 94 | } 95 | 96 | var name: String 97 | var path: String 98 | var timestamp: TimeInterval = 0 99 | var persistence: Bool 100 | var currentVersion: UInt32 = 0 101 | 102 | open private(set) var objectEncoding: UInt8 = RTMPConnection.defaultObjectEncoding 103 | open private(set) var data: [String: Any?] = [: ] 104 | 105 | private var succeeded: Bool = false { 106 | didSet { 107 | guard succeeded else { 108 | return 109 | } 110 | for (key, value) in data { 111 | setProperty(key, value) 112 | } 113 | } 114 | } 115 | 116 | override open var description: String { 117 | return data.description 118 | } 119 | 120 | private var rtmpConnection: RTMPConnection? 121 | 122 | init(name: String, path: String, persistence: Bool) { 123 | self.name = name 124 | self.path = path 125 | self.persistence = persistence 126 | super.init() 127 | } 128 | 129 | open func setProperty(_ name: String, _ value: Any?) { 130 | data[name] = value 131 | guard let rtmpConnection: RTMPConnection = rtmpConnection, succeeded else { 132 | return 133 | } 134 | rtmpConnection.socket.doOutput(chunk: createChunk([ 135 | RTMPSharedObjectEvent(type: .requestChange, name: name, data: value) 136 | ]), locked: nil) 137 | } 138 | 139 | open func connect(_ rtmpConnection: RTMPConnection) { 140 | if self.rtmpConnection != nil { 141 | close() 142 | } 143 | self.rtmpConnection = rtmpConnection 144 | rtmpConnection.addEventListener(Event.RTMP_STATUS, selector: #selector(RTMPSharedObject.rtmpStatusHandler(_: )), observer: self) 145 | if rtmpConnection.connected { 146 | timestamp = rtmpConnection.socket.timestamp 147 | rtmpConnection.socket.doOutput(chunk: createChunk([RTMPSharedObjectEvent(type: .use)]), locked: nil) 148 | } 149 | } 150 | 151 | open func clear() { 152 | data.removeAll(keepingCapacity: false) 153 | rtmpConnection?.socket.doOutput(chunk: createChunk([RTMPSharedObjectEvent(type: .clear)]), locked: nil) 154 | } 155 | 156 | open func close() { 157 | data.removeAll(keepingCapacity: false) 158 | rtmpConnection?.removeEventListener(Event.RTMP_STATUS, selector: #selector(RTMPSharedObject.rtmpStatusHandler(_: )), observer: self) 159 | rtmpConnection?.socket.doOutput(chunk: createChunk([RTMPSharedObjectEvent(type: .release)]), locked: nil) 160 | rtmpConnection = nil 161 | } 162 | 163 | final func on(message: RTMPSharedObjectMessage) { 164 | currentVersion = message.currentVersion 165 | var changeList: [[String: Any?]] = [] 166 | for event in message.events { 167 | var change: [String: Any?] = [ 168 | "code": "", 169 | "name": event.name, 170 | "oldValue": nil 171 | ] 172 | switch event.type { 173 | case .change: 174 | change["code"] = "change" 175 | change["oldValue"] = data.removeValue(forKey: event.name!) 176 | data[event.name!] = event.data 177 | case .success: 178 | change["code"] = "success" 179 | case .status: 180 | change["code"] = "reject" 181 | change["oldValue"] = data.removeValue(forKey: event.name!) 182 | case .clear: 183 | data.removeAll(keepingCapacity: false) 184 | change["code"] = "clear" 185 | case .remove: 186 | change["code"] = "delete" 187 | case .useSuccess: 188 | succeeded = true 189 | continue 190 | default: 191 | continue 192 | } 193 | changeList.append(change) 194 | } 195 | dispatch(Event.SYNC, bubbles: false, data: changeList) 196 | } 197 | 198 | func createChunk(_ events: [RTMPSharedObjectEvent]) -> RTMPChunk { 199 | let now: Date = Date() 200 | let timestamp: TimeInterval = now.timeIntervalSince1970 - self.timestamp 201 | self.timestamp = now.timeIntervalSince1970 202 | defer { 203 | currentVersion += 1 204 | } 205 | return RTMPChunk( 206 | type: succeeded ? .one : .zero, 207 | streamId: RTMPChunk.StreamID.command.rawValue, 208 | message: RTMPSharedObjectMessage( 209 | timestamp: UInt32(timestamp * 1000), 210 | objectEncoding: objectEncoding, 211 | sharedObjectName: name, 212 | currentVersion: succeeded ? 0 : currentVersion, 213 | flags: Data([persistence ? 0x01 : 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), 214 | events: events 215 | ) 216 | ) 217 | } 218 | 219 | @objc func rtmpStatusHandler(_ notification: Notification) { 220 | let e: Event = Event.from(notification) 221 | if let data: ASObject = e.data as? ASObject, let code: String = data["code"] as? String { 222 | switch code { 223 | case RTMPConnection.Code.connectSuccess.rawValue: 224 | timestamp = rtmpConnection!.socket.timestamp 225 | rtmpConnection!.socket.doOutput(chunk: createChunk([RTMPSharedObjectEvent(type: .use)]), locked: nil) 226 | default: 227 | break 228 | } 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/RTMP/RTMPSocket.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol RTMPSocketCompatible: class { 4 | var timeout: Int64 { get set } 5 | var connected: Bool { get } 6 | var timestamp: TimeInterval { get } 7 | var chunkSizeC: Int { get set } 8 | var chunkSizeS: Int { get set } 9 | var totalBytesIn: Int64 { get } 10 | var totalBytesOut: Int64 { get } 11 | var queueBytesOut: Int64 { get } 12 | var inputBuffer: Data { get set } 13 | var securityLevel: StreamSocketSecurityLevel { get set } 14 | weak var delegate: RTMPSocketDelegate? { get set } 15 | 16 | @discardableResult 17 | func doOutput(chunk: RTMPChunk, locked: UnsafeMutablePointer?) -> Int 18 | func close(isDisconnected: Bool) 19 | func connect(withName: String, port: Int) 20 | func deinitConnection(isDisconnected: Bool) 21 | } 22 | 23 | // MARK: - 24 | protocol RTMPSocketDelegate: IEventDispatcher { 25 | func listen(_ data: Data) 26 | func didSetReadyState(_ readyState: RTMPSocket.ReadyState) 27 | func didSetTotalBytesIn(_ totalBytesIn: Int64) 28 | } 29 | 30 | // MARK: - 31 | final class RTMPSocket: NetSocket, RTMPSocketCompatible { 32 | enum ReadyState: UInt8 { 33 | case uninitialized = 0 34 | case versionSent = 1 35 | case ackSent = 2 36 | case handshakeDone = 3 37 | case closing = 4 38 | case closed = 5 39 | } 40 | 41 | var readyState: ReadyState = .uninitialized { 42 | didSet { 43 | delegate?.didSetReadyState(readyState) 44 | } 45 | } 46 | var timestamp: TimeInterval { 47 | return handshake.timestamp 48 | } 49 | var chunkSizeC: Int = RTMPChunk.defaultSize 50 | var chunkSizeS: Int = RTMPChunk.defaultSize 51 | weak var delegate: RTMPSocketDelegate? 52 | override var totalBytesIn: Int64 { 53 | didSet { 54 | delegate?.didSetTotalBytesIn(totalBytesIn) 55 | } 56 | } 57 | 58 | override var connected: Bool { 59 | didSet { 60 | if connected { 61 | doOutput(data: handshake.c0c1packet) 62 | readyState = .versionSent 63 | return 64 | } 65 | readyState = .closed 66 | for event in events { 67 | delegate?.dispatch(event: event) 68 | } 69 | events.removeAll() 70 | } 71 | } 72 | 73 | private var events: [Event] = [] 74 | private var handshake: RTMPHandshake = RTMPHandshake() 75 | 76 | @discardableResult 77 | func doOutput(chunk: RTMPChunk, locked: UnsafeMutablePointer? = nil) -> Int { 78 | let chunks: [Data] = chunk.split(chunkSizeS) 79 | for i in 0..= 2) { 81 | 82 | if CommandLine.arguments[1] == "start" { 83 | service.startService() 84 | } 85 | 86 | if CommandLine.arguments.count >= 3 { 87 | let minute = (CommandLine.arguments[2] as NSString).floatValue 88 | let date = Date().addingTimeInterval(TimeInterval( minute * 60.0 )) 89 | RunLoop.current.run(until: date) 90 | RunLoop.current.perform(#selector(service.stopService), with: nil, afterDelay: TimeInterval( minute * 60.0 )) 91 | } else { 92 | RunLoop.current.run() 93 | } 94 | } 95 | else { 96 | print("please provide valid arguments") 97 | exit(0) 98 | } 99 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Util/AnyUtil.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class AnyUtil { 4 | static func isZero(_ value: Any) -> Bool { 5 | if let value: Int = value as? Int { 6 | return value == 0 7 | } 8 | if let value: Double = value as? Double { 9 | return value == 0 10 | } 11 | return false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Util/CRC32.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class CRC32 { 4 | static let MPEG2: CRC32 = CRC32(polynomial: 0x04c11db7) 5 | 6 | let table: [UInt32] 7 | 8 | init(polynomial: UInt32) { 9 | var table: [UInt32] = [UInt32](repeating: 0x00000000, count: 256) 10 | for i in 0.. UInt32 { 21 | return calculate(data, seed: nil) 22 | } 23 | 24 | func calculate(_ data: Data, seed: UInt32?) -> UInt32 { 25 | var crc: UInt32 = seed ?? 0xffffffff 26 | for i in 0..> 24) ^ (UInt32(data[i]) & 0xff) & 0xff)] 28 | } 29 | return crc 30 | } 31 | } 32 | 33 | extension CRC32: CustomStringConvertible { 34 | // MARK: CustomStringConvertible 35 | var description: String { 36 | return Mirror(reflecting: self).description 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Util/DeviceUtil.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | 4 | #if os(iOS) || os(macOS) 5 | public final class DeviceUtil { 6 | private init() { 7 | } 8 | 9 | static public func device(withPosition: AVCaptureDevice.Position) -> AVCaptureDevice? { 10 | for device in AVCaptureDevice.devices() { 11 | if device.hasMediaType(AVMediaType.video) && device.position == withPosition { 12 | return device 13 | } 14 | } 15 | return nil 16 | } 17 | 18 | static public func device(withLocalizedName: String, mediaType: String) -> AVCaptureDevice? { 19 | for device in AVCaptureDevice.devices() { 20 | if device.hasMediaType(AVMediaType(rawValue: mediaType)) && device.localizedName == withLocalizedName { 21 | return device 22 | } 23 | } 24 | return nil 25 | } 26 | 27 | static func getActualFPS(_ fps: Float64, device: AVCaptureDevice) -> (fps: Float64, duration: CMTime)? { 28 | var durations: [CMTime] = [] 29 | var frameRates: [Float64] = [] 30 | 31 | for object: Any in device.activeFormat.videoSupportedFrameRateRanges { 32 | guard let range: AVFrameRateRange = object as? AVFrameRateRange else { 33 | continue 34 | } 35 | if range.minFrameRate == range.maxFrameRate { 36 | durations.append(range.minFrameDuration) 37 | frameRates.append(range.maxFrameRate) 38 | continue 39 | } 40 | if range.minFrameRate <= fps && fps <= range.maxFrameRate { 41 | return (fps, CMTimeMake(100, Int32(100 * fps))) 42 | } 43 | 44 | let actualFPS: Float64 = max(range.minFrameRate, min(range.maxFrameRate, fps)) 45 | return (actualFPS, CMTimeMake(100, Int32(100 * actualFPS))) 46 | } 47 | 48 | var diff: [Float64] = [] 49 | for frameRate in frameRates { 50 | diff.append(abs(frameRate - fps)) 51 | } 52 | if let minElement: Float64 = diff.min() { 53 | for i in 0.., inOutputTIme: UnsafePointer, flagsIn: CVOptionFlags, flgasOut: UnsafeMutablePointer, displayLinkContext: UnsafeMutableRawPointer?) -> CVReturn in 16 | let displayLink: DisplayLink = Unmanaged.fromOpaque(displayLinkContext!).takeUnretainedValue() 17 | displayLink.timestamp = Double(inNow.pointee.videoTime) / Double(inNow.pointee.videoTimeScale) 18 | _ = displayLink.delegate?.perform(displayLink.selector, with: displayLink) 19 | return 0 20 | } 21 | 22 | init(target: NSObject, selector sel: Selector) { 23 | super.init() 24 | status = CVDisplayLinkCreateWithActiveCGDisplays(&displayLink) 25 | guard let displayLink: CVDisplayLink = displayLink else { 26 | return 27 | } 28 | self.delegate = target 29 | self.selector = sel 30 | CVDisplayLinkSetOutputCallback(displayLink, callback, Unmanaged.passUnretained(self).toOpaque()) 31 | } 32 | 33 | func add(to runloop: RunLoop, forMode mode: RunLoopMode) { 34 | guard let displayLink: CVDisplayLink = displayLink else { 35 | return 36 | } 37 | status = CVDisplayLinkStart(displayLink) 38 | } 39 | 40 | func invalidate() { 41 | guard let displayLink: CVDisplayLink = displayLink else { 42 | return 43 | } 44 | status = CVDisplayLinkStop(displayLink) 45 | } 46 | } 47 | #else 48 | typealias DisplayLink = CADisplayLink 49 | #endif 50 | 51 | protocol DisplayLinkedQueueDelegate: class { 52 | func queue(_ buffer: CMSampleBuffer) 53 | } 54 | 55 | final class DisplayLinkedQueue: NSObject { 56 | var running: Bool = false 57 | var bufferTime: TimeInterval = 0.1 // sec 58 | weak var delegate: DisplayLinkedQueueDelegate? 59 | private(set) var duration: TimeInterval = 0 60 | 61 | private var isReady: Bool = false 62 | private var buffers: [CMSampleBuffer] = [] 63 | private var mediaTime: CFTimeInterval = 0 64 | private var displayLink: DisplayLink? { 65 | didSet { 66 | if let oldValue: DisplayLink = oldValue { 67 | oldValue.invalidate() 68 | } 69 | guard let displayLink: DisplayLink = displayLink else { 70 | return 71 | } 72 | displayLink.frameInterval = 1 73 | displayLink.add(to: .main, forMode: .commonModes) 74 | } 75 | } 76 | private let lockQueue: DispatchQueue = DispatchQueue(label: "com.haishinkit.HaishinKit.DisplayLinkedQueue.lock") 77 | 78 | func enqueue(_ buffer: CMSampleBuffer) { 79 | lockQueue.async { 80 | self.duration += buffer.duration.seconds 81 | self.buffers.append(buffer) 82 | if !self.isReady { 83 | self.isReady = self.duration <= self.bufferTime 84 | } 85 | } 86 | } 87 | 88 | @objc func update(displayLink: DisplayLink) { 89 | guard let first: CMSampleBuffer = buffers.first, isReady else { 90 | return 91 | } 92 | if mediaTime == 0 { 93 | mediaTime = displayLink.timestamp 94 | } 95 | if first.presentationTimeStamp.seconds <= displayLink.timestamp - mediaTime { 96 | lockQueue.async { 97 | self.buffers.removeFirst() 98 | } 99 | delegate?.queue(first) 100 | } 101 | } 102 | } 103 | 104 | extension DisplayLinkedQueue: Running { 105 | // MARK: Running 106 | func startRunning() { 107 | lockQueue.async { 108 | guard !self.running else { 109 | return 110 | } 111 | self.displayLink = DisplayLink(target: self, selector: #selector(self.update(displayLink:))) 112 | self.running = false 113 | } 114 | } 115 | 116 | func stopRunning() { 117 | lockQueue.async { 118 | guard self.running else { 119 | return 120 | } 121 | self.displayLink = nil 122 | self.buffers.removeAll() 123 | self.running = false 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Util/EventDispatcher.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | flash.events.IEventDispatcher for Swift 5 | */ 6 | public protocol IEventDispatcher: class { 7 | func addEventListener(_ type: String, selector: Selector, observer: AnyObject?, useCapture: Bool) 8 | func removeEventListener(_ type: String, selector: Selector, observer: AnyObject?, useCapture: Bool) 9 | func dispatch(event: Event) 10 | func dispatch(_ type: String, bubbles: Bool, data: Any?) 11 | } 12 | 13 | public enum EventPhase: UInt8 { 14 | case capturing = 0 15 | case atTarget = 1 16 | case bubbling = 2 17 | case dispose = 3 18 | } 19 | 20 | // MARK: - 21 | /** 22 | flash.events.Event for Swift 23 | */ 24 | open class Event: NSObject { 25 | open static let SYNC: String = "sync" 26 | open static let EVENT: String = "event" 27 | open static let IO_ERROR: String = "ioError" 28 | open static let RTMP_STATUS: String = "rtmpStatus" 29 | 30 | open static func from(_ notification: Notification) -> Event { 31 | guard 32 | let userInfo: [AnyHashable: Any] = notification.userInfo, 33 | let event: Event = userInfo["event"] as? Event else { 34 | return Event(type: Event.EVENT) 35 | } 36 | return event 37 | } 38 | 39 | open fileprivate(set) var type: String 40 | open fileprivate(set) var bubbles: Bool 41 | open fileprivate(set) var data: Any? 42 | open fileprivate(set) var target: AnyObject? 43 | 44 | open override var description: String { 45 | return Mirror(reflecting: self).description 46 | } 47 | 48 | public init(type: String, bubbles: Bool = false, data: Any? = nil) { 49 | self.type = type 50 | self.bubbles = bubbles 51 | self.data = data 52 | } 53 | } 54 | 55 | // MARK: - 56 | /** 57 | flash.events.EventDispatcher for Swift 58 | */ 59 | open class EventDispatcher: NSObject, IEventDispatcher { 60 | 61 | private weak var target: AnyObject? 62 | 63 | override public init() { 64 | super.init() 65 | } 66 | 67 | public init(target: AnyObject) { 68 | self.target = target 69 | } 70 | 71 | deinit { 72 | target = nil 73 | } 74 | 75 | public final func addEventListener(_ type: String, selector: Selector, observer: AnyObject? = nil, useCapture: Bool = false) { 76 | NotificationCenter.default.addObserver( 77 | observer ?? target ?? self, selector: selector, name: NSNotification.Name(rawValue: "\(type)/\(useCapture)"), object: target ?? self 78 | ) 79 | } 80 | 81 | public final func removeEventListener(_ type: String, selector: Selector, observer: AnyObject? = nil, useCapture: Bool = false) { 82 | NotificationCenter.default.removeObserver( 83 | observer ?? target ?? self, name: NSNotification.Name(rawValue: "\(type)/\(useCapture)"), object: target ?? self 84 | ) 85 | } 86 | 87 | open func dispatch(event: Event) { 88 | event.target = target ?? self 89 | NotificationCenter.default.post( 90 | name: Notification.Name(rawValue: "\(event.type)/false"), object: target ?? self, userInfo: ["event": event] 91 | ) 92 | event.target = nil 93 | } 94 | 95 | public final func dispatch(_ type: String, bubbles: Bool, data: Any?) { 96 | dispatch(event: Event(type: type, bubbles: bubbles, data: data)) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Util/MD5.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | Message Digest Algorithm 5 5 | 6 | - seealso: 7 | - https: //ja.wikipedia.org/wiki/MD5 8 | - https: //www.ietf.org/rfc/rfc1321.txt 9 | */ 10 | final class MD5 { 11 | 12 | static let a: UInt32 = 0x67452301 13 | static let b: UInt32 = 0xefcdab89 14 | static let c: UInt32 = 0x98badcfe 15 | static let d: UInt32 = 0x10325476 16 | 17 | static let S11: UInt32 = 7 18 | static let S12: UInt32 = 12 19 | static let S13: UInt32 = 17 20 | static let S14: UInt32 = 22 21 | static let S21: UInt32 = 5 22 | static let S22: UInt32 = 9 23 | static let S23: UInt32 = 14 24 | static let S24: UInt32 = 20 25 | static let S31: UInt32 = 4 26 | static let S32: UInt32 = 11 27 | static let S33: UInt32 = 16 28 | static let S34: UInt32 = 23 29 | static let S41: UInt32 = 6 30 | static let S42: UInt32 = 10 31 | static let S43: UInt32 = 15 32 | static let S44: UInt32 = 21 33 | 34 | struct Context { 35 | var a: UInt32 = MD5.a 36 | var b: UInt32 = MD5.b 37 | var c: UInt32 = MD5.c 38 | var d: UInt32 = MD5.d 39 | 40 | mutating func FF(_ x: UInt32, _ s: UInt32, _ k: UInt32) { 41 | let swap: UInt32 = d 42 | let F: UInt32 = (b & c) | ((~b) & d) 43 | d = c 44 | c = b 45 | b = b &+ rotateLeft(a &+ F &+ k &+ x, s) 46 | a = swap 47 | } 48 | 49 | mutating func GG(_ x: UInt32, _ s: UInt32, _ k: UInt32) { 50 | let swap: UInt32 = d 51 | let G: UInt32 = (d & b) | (c & (~d)) 52 | d = c 53 | c = b 54 | b = b &+ rotateLeft(a &+ G &+ k &+ x, s) 55 | a = swap 56 | } 57 | 58 | mutating func HH(_ x: UInt32, _ s: UInt32, _ k: UInt32) { 59 | let swap: UInt32 = d 60 | let H: UInt32 = b ^ c ^ d 61 | d = c 62 | c = b 63 | b = b &+ rotateLeft(a &+ H &+ k &+ x, s) 64 | a = swap 65 | } 66 | 67 | mutating func II(_ x: UInt32, _ s: UInt32, _ k: UInt32) { 68 | let swap: UInt32 = d 69 | let I: UInt32 = c ^ (b | (~d)) 70 | d = c 71 | c = b 72 | b = b &+ rotateLeft(a &+ I &+ k &+ x, s) 73 | a = swap 74 | } 75 | 76 | func rotateLeft(_ x: UInt32, _ n: UInt32) -> UInt32 { 77 | return ((x << n) & 0xFFFFFFFF) | (x >> (32 - n)) 78 | } 79 | 80 | var data: Data { 81 | return a.data + b.data + c.data + d.data 82 | } 83 | } 84 | 85 | static func base64(_ message: String) -> String { 86 | return calculate(message).base64EncodedString(options: .lineLength64Characters) 87 | } 88 | 89 | static func calculate(_ message: String) -> Data { 90 | return calculate(ByteArray().writeUTF8Bytes(message).data) 91 | } 92 | 93 | static func calculate(_ data: Data) -> Data { 94 | var context: Context = Context() 95 | 96 | let count: Data = UInt64(data.count * 8).bigEndian.data 97 | let message: ByteArray = ByteArray(data: data + [0x80]) 98 | message.length += 64 - (message.length % 64) 99 | message[message.length - 8] = count[7] 100 | message[message.length - 7] = count[6] 101 | message[message.length - 6] = count[5] 102 | message[message.length - 5] = count[4] 103 | message[message.length - 4] = count[3] 104 | message[message.length - 3] = count[2] 105 | message[message.length - 2] = count[1] 106 | message[message.length - 1] = count[0] 107 | 108 | message.sequence(64) { 109 | let x: [UInt32] = $0.toUInt32() 110 | 111 | guard x.count == 16 else { 112 | return 113 | } 114 | 115 | var ctx: Context = Context() 116 | ctx.a = context.a 117 | ctx.b = context.b 118 | ctx.c = context.c 119 | ctx.d = context.d 120 | 121 | /* Round 1 */ 122 | ctx.FF(x[ 0], S11, 0xd76aa478) 123 | ctx.FF(x[ 1], S12, 0xe8c7b756) 124 | ctx.FF(x[ 2], S13, 0x242070db) 125 | ctx.FF(x[ 3], S14, 0xc1bdceee) 126 | ctx.FF(x[ 4], S11, 0xf57c0faf) 127 | ctx.FF(x[ 5], S12, 0x4787c62a) 128 | ctx.FF(x[ 6], S13, 0xa8304613) 129 | ctx.FF(x[ 7], S14, 0xfd469501) 130 | ctx.FF(x[ 8], S11, 0x698098d8) 131 | ctx.FF(x[ 9], S12, 0x8b44f7af) 132 | ctx.FF(x[10], S13, 0xffff5bb1) 133 | ctx.FF(x[11], S14, 0x895cd7be) 134 | ctx.FF(x[12], S11, 0x6b901122) 135 | ctx.FF(x[13], S12, 0xfd987193) 136 | ctx.FF(x[14], S13, 0xa679438e) 137 | ctx.FF(x[15], S14, 0x49b40821) 138 | 139 | /* Round 2 */ 140 | ctx.GG(x[ 1], S21, 0xf61e2562) 141 | ctx.GG(x[ 6], S22, 0xc040b340) 142 | ctx.GG(x[11], S23, 0x265e5a51) 143 | ctx.GG(x[ 0], S24, 0xe9b6c7aa) 144 | ctx.GG(x[ 5], S21, 0xd62f105d) 145 | ctx.GG(x[10], S22, 0x2441453) 146 | ctx.GG(x[15], S23, 0xd8a1e681) 147 | ctx.GG(x[ 4], S24, 0xe7d3fbc8) 148 | ctx.GG(x[ 9], S21, 0x21e1cde6) 149 | ctx.GG(x[14], S22, 0xc33707d6) 150 | ctx.GG(x[ 3], S23, 0xf4d50d87) 151 | ctx.GG(x[ 8], S24, 0x455a14ed) 152 | ctx.GG(x[13], S21, 0xa9e3e905) 153 | ctx.GG(x[ 2], S22, 0xfcefa3f8) 154 | ctx.GG(x[ 7], S23, 0x676f02d9) 155 | ctx.GG(x[12], S24, 0x8d2a4c8a) 156 | 157 | /* Round 3 */ 158 | ctx.HH(x[ 5], S31, 0xfffa3942) 159 | ctx.HH(x[ 8], S32, 0x8771f681) 160 | ctx.HH(x[11], S33, 0x6d9d6122) 161 | ctx.HH(x[14], S34, 0xfde5380c) 162 | ctx.HH(x[ 1], S31, 0xa4beea44) 163 | ctx.HH(x[ 4], S32, 0x4bdecfa9) 164 | ctx.HH(x[ 7], S33, 0xf6bb4b60) 165 | ctx.HH(x[10], S34, 0xbebfbc70) 166 | ctx.HH(x[13], S31, 0x289b7ec6) 167 | ctx.HH(x[ 0], S32, 0xeaa127fa) 168 | ctx.HH(x[ 3], S33, 0xd4ef3085) 169 | ctx.HH(x[ 6], S34, 0x4881d05) 170 | ctx.HH(x[ 9], S31, 0xd9d4d039) 171 | ctx.HH(x[12], S32, 0xe6db99e5) 172 | ctx.HH(x[15], S33, 0x1fa27cf8) 173 | ctx.HH(x[ 2], S34, 0xc4ac5665) 174 | 175 | /* Round 4 */ 176 | ctx.II(x[ 0], S41, 0xf4292244) 177 | ctx.II(x[ 7], S42, 0x432aff97) 178 | ctx.II(x[14], S43, 0xab9423a7) 179 | ctx.II(x[ 5], S44, 0xfc93a039) 180 | ctx.II(x[12], S41, 0x655b59c3) 181 | ctx.II(x[ 3], S42, 0x8f0ccc92) 182 | ctx.II(x[10], S43, 0xffeff47d) 183 | ctx.II(x[ 1], S44, 0x85845dd1) 184 | ctx.II(x[ 8], S41, 0x6fa87e4f) 185 | ctx.II(x[15], S42, 0xfe2ce6e0) 186 | ctx.II(x[ 6], S43, 0xa3014314) 187 | ctx.II(x[13], S44, 0x4e0811a1) 188 | ctx.II(x[ 4], S41, 0xf7537e82) 189 | ctx.II(x[11], S42, 0xbd3af235) 190 | ctx.II(x[ 2], S43, 0x2ad7d2bb) 191 | ctx.II(x[ 9], S44, 0xeb86d391) 192 | 193 | context.a = context.a &+ ctx.a 194 | context.b = context.b &+ ctx.b 195 | context.c = context.c &+ ctx.c 196 | context.d = context.d &+ ctx.d 197 | } 198 | 199 | return context.data 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Util/MachUtil.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct MachUtil { 4 | static public let nanosPerUsec: UInt64 = 1000 5 | static public let nanosPerMsec: UInt64 = 1000 * 1000 6 | static public let nanosPerSec: UInt64 = 1000 * 1000 * 1000 7 | 8 | private static var timebase: mach_timebase_info_data_t = { 9 | var timebase: mach_timebase_info_data_t = mach_timebase_info_data_t() 10 | mach_timebase_info(&timebase) 11 | return timebase 12 | }() 13 | 14 | static public func nanosToAbs(_ nanos: UInt64) -> UInt64 { 15 | return nanos * UInt64(timebase.denom) / UInt64(timebase.numer) 16 | } 17 | 18 | static public func absToNanos(_ abs: UInt64) -> UInt64 { 19 | return abs * UInt64(timebase.numer) / UInt64(timebase.denom) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Util/Mutex.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Mutex: NSLocking { 4 | 5 | enum Error: Swift.Error { 6 | case inval 7 | case busy 8 | case again 9 | case deadlnk 10 | case perm 11 | } 12 | 13 | private let mutex: UnsafeMutablePointer 14 | private let condition: UnsafeMutablePointer 15 | private let attribute: UnsafeMutablePointer 16 | 17 | init() { 18 | mutex = UnsafeMutablePointer.allocate(capacity: MemoryLayout.size) 19 | condition = UnsafeMutablePointer.allocate(capacity: MemoryLayout.size) 20 | attribute = UnsafeMutablePointer.allocate(capacity: MemoryLayout.size) 21 | 22 | pthread_mutexattr_init(attribute) 23 | pthread_mutex_init(mutex, attribute) 24 | pthread_cond_init(condition, nil) 25 | } 26 | 27 | deinit { 28 | pthread_cond_destroy(condition) 29 | pthread_mutexattr_destroy(attribute) 30 | pthread_mutex_destroy(mutex) 31 | } 32 | 33 | func tryLock() throws { 34 | switch pthread_mutex_trylock(mutex) { 35 | case EBUSY: 36 | throw Mutex.Error.busy 37 | case EINVAL: 38 | throw Mutex.Error.inval 39 | default: 40 | break 41 | } 42 | } 43 | 44 | func lock() { 45 | pthread_mutex_lock(mutex) 46 | } 47 | 48 | func unlock() { 49 | pthread_mutex_unlock(mutex) 50 | } 51 | 52 | func wait() -> Bool { 53 | return pthread_cond_wait(condition, mutex) == 0 54 | } 55 | 56 | func signal() -> Bool { 57 | return pthread_cond_signal(condition) == 0 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Util/TimerDriver.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol TimerDriverDelegate: class { 4 | func tick(_ driver: TimerDriver) 5 | } 6 | 7 | // MARK: - 8 | public class TimerDriver: NSObject { 9 | public var interval: UInt64 = MachUtil.nanosToAbs(10 * MachUtil.nanosPerMsec) 10 | 11 | var queue: DispatchQueue? 12 | weak var delegate: TimerDriverDelegate? 13 | 14 | private var runloop: RunLoop? 15 | private var nextFire: UInt64 = 0 16 | private weak var timer: Timer? { 17 | didSet { 18 | if let oldValue: Timer = oldValue { 19 | oldValue.invalidate() 20 | } 21 | if let timer: Timer = timer { 22 | RunLoop.current.add(timer, forMode: .commonModes) 23 | } 24 | } 25 | } 26 | 27 | public override var description: String { 28 | return Mirror(reflecting: self).description 29 | } 30 | 31 | public func setDelegate(_ delegate: TimerDriverDelegate, withQueue: DispatchQueue? = nil) { 32 | self.delegate = delegate 33 | self.queue = withQueue 34 | } 35 | 36 | @objc func on(timer: Timer) { 37 | guard nextFire <= mach_absolute_time() else { 38 | return 39 | } 40 | if let queue: DispatchQueue = queue { 41 | queue.sync { 42 | self.delegate?.tick(self) 43 | } 44 | } else { 45 | delegate?.tick(self) 46 | } 47 | nextFire += interval 48 | } 49 | } 50 | 51 | extension TimerDriver: Running { 52 | // MARK: Running 53 | public var running: Bool { 54 | return runloop != nil 55 | } 56 | 57 | final public func startRunning() { 58 | DispatchQueue.global(qos: .userInteractive).async { 59 | if let _: RunLoop = self.runloop { 60 | return 61 | } 62 | self.timer = Timer( 63 | timeInterval: 0.0001, target: self, selector: #selector(TimerDriver.on(timer: )), userInfo: nil, repeats: true 64 | ) 65 | self.nextFire = mach_absolute_time() + self.interval 66 | self.delegate?.tick(self) 67 | self.runloop = .current 68 | self.runloop?.run() 69 | } 70 | } 71 | 72 | final public func stopRunning() { 73 | guard let runloop: RunLoop = runloop else { 74 | return 75 | } 76 | timer = nil 77 | CFRunLoopStop(runloop.getCFRunLoop()) 78 | self.runloop = nil 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/Util/VideoGravityUtil.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | 4 | final class VideoGravityUtil { 5 | @inline(__always) static func calclute(_ videoGravity: AVLayerVideoGravity, inRect: inout CGRect, fromRect: inout CGRect) { 6 | switch videoGravity { 7 | case .resizeAspect: 8 | resizeAspect(&inRect, fromRect: &fromRect) 9 | case .resizeAspectFill: 10 | resizeAspectFill(&inRect, fromRect: &fromRect) 11 | default: 12 | break 13 | } 14 | } 15 | 16 | @inline(__always) static func resizeAspect(_ inRect: inout CGRect, fromRect: inout CGRect) { 17 | let xRatio: CGFloat = inRect.width / fromRect.width 18 | let yRatio: CGFloat = inRect.height / fromRect.height 19 | if yRatio < xRatio { 20 | inRect.origin.x = (inRect.size.width - fromRect.size.width * yRatio) / 2 21 | inRect.size.width = fromRect.size.width * yRatio 22 | } else { 23 | inRect.origin.y = (inRect.size.height - fromRect.size.height * xRatio) / 2 24 | inRect.size.height = fromRect.size.height * xRatio 25 | } 26 | } 27 | 28 | @inline(__always) static func resizeAspectFill(_ inRect: inout CGRect, fromRect: inout CGRect) { 29 | let inRectAspect: CGFloat = inRect.size.width / inRect.size.height 30 | let fromRectAspect: CGFloat = fromRect.size.width / fromRect.size.height 31 | if inRectAspect < fromRectAspect { 32 | inRect.origin.x += (inRect.size.width - inRect.size.height * fromRectAspect) / 2 33 | inRect.size.width = inRect.size.height * fromRectAspect 34 | } else { 35 | inRect.origin.y += (inRect.size.height - inRect.size.width / fromRectAspect) / 2 36 | inRect.size.height = inRect.size.width / fromRectAspect 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/macOS/AudioUtil.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | import CoreAudio 4 | 5 | final class AudioUtil { 6 | private static var defaultDeviceID: AudioObjectID { 7 | var deviceID: AudioObjectID = AudioObjectID(0) 8 | var size: UInt32 = UInt32(MemoryLayout.size) 9 | var address: AudioObjectPropertyAddress = AudioObjectPropertyAddress() 10 | address.mSelector = kAudioHardwarePropertyDefaultOutputDevice 11 | address.mScope = kAudioObjectPropertyScopeGlobal 12 | address.mElement = kAudioObjectPropertyElementMaster 13 | AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size, &deviceID) 14 | return deviceID 15 | } 16 | 17 | private init() { 18 | } 19 | 20 | static func setInputGain(_ volume: Float32) -> OSStatus { 21 | var inputVolume: Float32 = volume 22 | let size: UInt32 = UInt32(MemoryLayout.size) 23 | var address: AudioObjectPropertyAddress = AudioObjectPropertyAddress() 24 | address.mSelector = kAudioDevicePropertyVolumeScalar 25 | address.mScope = kAudioObjectPropertyScopeGlobal 26 | address.mElement = kAudioObjectPropertyElementMaster 27 | return AudioObjectSetPropertyData(defaultDeviceID, &address, 0, nil, size, &inputVolume) 28 | } 29 | 30 | static func getInputGain() -> Float32 { 31 | var volume: Float32 = 0.5 32 | var size: UInt32 = UInt32(MemoryLayout.size) 33 | var address: AudioObjectPropertyAddress = AudioObjectPropertyAddress() 34 | address.mSelector = kAudioDevicePropertyVolumeScalar 35 | address.mScope = kAudioObjectPropertyScopeGlobal 36 | address.mElement = kAudioObjectPropertyElementMaster 37 | AudioObjectGetPropertyData(defaultDeviceID, &address, 0, nil, &size, &volume) 38 | return volume 39 | } 40 | 41 | static func startRunning() { 42 | } 43 | 44 | static func stopRunning() { 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/macOS/NetStream+Extenstion.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | 4 | extension NetStream { 5 | open func attachScreen(_ screen: AVCaptureScreenInput?) { 6 | lockQueue.async { 7 | self.mixer.videoIO.attachScreen(screen) 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/Recorder/macOS/VideoIOComponent+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | 4 | extension VideoIOComponent { 5 | func attachScreen(_ screen: AVCaptureScreenInput?) { 6 | mixer?.session.beginConfiguration() 7 | output = nil 8 | guard let _: AVCaptureScreenInput = screen else { 9 | input = nil 10 | return 11 | } 12 | input = screen 13 | mixer?.session.addOutput(output) 14 | output.setSampleBufferDelegate(self, queue: lockQueue) 15 | mixer?.session.commitConfiguration() 16 | if mixer?.session.isRunning ?? false { 17 | mixer?.session.startRunning() 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/showPairing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // Bluetooth 4 | // 5 | // Created by Sumit Chudasama on 06/03/2018. 6 | // Copyright © 2018 Sumit Chudasama. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import IOBluetooth 11 | import Cocoa 12 | 13 | var defaultBluetoothDeviceName : String = ""//"ABC's Macbook Pro" 14 | 15 | class BluetoothDeviceInquiryDelegate : IOBluetoothDeviceInquiryDelegate { 16 | func deviceInquiryStarted(_ sender: IOBluetoothDeviceInquiry) { 17 | } 18 | func deviceInquiryDeviceFound(_ sender: IOBluetoothDeviceInquiry, device: IOBluetoothDevice) { 19 | guard let addressString = device.addressString, 20 | let deviceName = device.name 21 | else { 22 | return 23 | } 24 | 25 | if deviceName.range(of:defaultBluetoothDeviceName) != nil { 26 | 27 | let device : IOBluetoothDevice = IOBluetoothDevice(addressString: addressString) 28 | let pair : IOBluetoothDevicePair = IOBluetoothDevicePair.init(device: device) 29 | pair.replyUserConfirmation(true) 30 | let deviceReturn : IOReturn = pair.start() 31 | 32 | if deviceReturn == kIOReturnSuccess { 33 | print("Pairing alert presented successfully.") 34 | exit(0) 35 | } else { 36 | print("Please try again.") 37 | exit(-2) 38 | } 39 | } 40 | } 41 | 42 | func deviceInquiryComplete(_ sender: IOBluetoothDeviceInquiry!, error: IOReturn, aborted: Bool) { 43 | print("No deviced found matching name : " + defaultBluetoothDeviceName) 44 | exit(-3) 45 | } 46 | } 47 | 48 | let bluetoothDeviceInquiryDelegate = BluetoothDeviceInquiryDelegate() 49 | let ibdi = IOBluetoothDeviceInquiry(delegate: bluetoothDeviceInquiryDelegate) 50 | 51 | if (CommandLine.arguments.count == 2 || !defaultBluetoothDeviceName.isEmpty) { 52 | 53 | if defaultBluetoothDeviceName.isEmpty { 54 | defaultBluetoothDeviceName = CommandLine.arguments[1].removingPercentEncoding! 55 | } 56 | 57 | ibdi?.updateNewDeviceNames = true 58 | let ibdiStart = ibdi?.start() 59 | 60 | if ibdiStart != kIOReturnSuccess { 61 | print("Please check bluetooth is enabled or not") 62 | exit(-1) 63 | } 64 | } 65 | else { 66 | print("Please provide bluetooth device name.") 67 | exit(-1) 68 | } 69 | 70 | RunLoop.current.run() 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/Commands/SwiftScripts/toggleBluetooth.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // Bluetooth 4 | // 5 | // Created by Sumit Chudasama on 06/03/2018. 6 | // Copyright © 2018 Sumit Chudasama. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import IOBluetooth 11 | import Cocoa 12 | 13 | var defaultBluetoothDeviceName : String = ""//"ABC's Macbook Pro" 14 | 15 | class BluetoothDeviceInquiryDelegate : IOBluetoothDeviceInquiryDelegate { 16 | func deviceInquiryStarted(_ sender: IOBluetoothDeviceInquiry) { 17 | } 18 | func deviceInquiryDeviceFound(_ sender: IOBluetoothDeviceInquiry, device: IOBluetoothDevice) { 19 | guard let addressString = device.addressString, 20 | let deviceName = device.name 21 | else { 22 | return 23 | } 24 | 25 | if deviceName.range(of:defaultBluetoothDeviceName) != nil { 26 | 27 | let device : IOBluetoothDevice = IOBluetoothDevice(addressString: addressString) 28 | toogleConnection(device: device) 29 | } 30 | } 31 | 32 | func deviceInquiryComplete(_ sender: IOBluetoothDeviceInquiry!, error: IOReturn, aborted: Bool) { 33 | print("No deviced found matching name : " + defaultBluetoothDeviceName) 34 | 35 | if IOBluetoothDevice.pairedDevices().count == 0 { 36 | print("No Paired Devices") 37 | } else { 38 | IOBluetoothDevice.pairedDevices().forEach({(device) in 39 | guard let device = device as? IOBluetoothDevice, 40 | let addressString = device.addressString, 41 | let deviceName = device.name 42 | else { 43 | return 44 | } 45 | 46 | if deviceName.range(of:defaultBluetoothDeviceName) != nil { 47 | let device : IOBluetoothDevice = IOBluetoothDevice(addressString: addressString) 48 | toogleConnection(device: device) 49 | } 50 | print("\(deviceName) => \(addressString)") 51 | }) 52 | } 53 | 54 | exit(-3) 55 | } 56 | } 57 | 58 | func toogleConnection(device: IOBluetoothDevice) { 59 | if !device.isPaired() { 60 | print("Not paired to device") 61 | exit(-2) 62 | } 63 | 64 | if device.isConnected() { 65 | print("closed connection") 66 | device.closeConnection() 67 | } 68 | else { 69 | print("opened connection") 70 | device.openConnection() 71 | } 72 | 73 | exit(0) 74 | } 75 | 76 | let bluetoothDeviceInquiryDelegate = BluetoothDeviceInquiryDelegate() 77 | let ibdi = IOBluetoothDeviceInquiry(delegate: bluetoothDeviceInquiryDelegate) 78 | 79 | if (CommandLine.arguments.count == 2 || !defaultBluetoothDeviceName.isEmpty) { 80 | 81 | if defaultBluetoothDeviceName.isEmpty { 82 | defaultBluetoothDeviceName = CommandLine.arguments[1].removingPercentEncoding! 83 | } 84 | 85 | ibdi?.updateNewDeviceNames = true 86 | let ibdiStart = ibdi?.start() 87 | 88 | if ibdiStart != kIOReturnSuccess { 89 | print("Please check bluetooth is enabled or not") 90 | exit(-1) 91 | } 92 | } 93 | else { 94 | print("Please provide bluetooth device name.") 95 | exit(-1) 96 | } 97 | 98 | RunLoop.current.run() 99 | 100 | -------------------------------------------------------------------------------- /src/Commands/commands.js: -------------------------------------------------------------------------------- 1 | var powerCommands = { 2 | halt: { 3 | command: ['halt'], 4 | sudo: true 5 | }, 6 | shutdown: { 7 | command: ['shutdown', '-h'], 8 | sudo: true 9 | }, 10 | restart: { 11 | command: ['shutdown', '-r'], 12 | sudo: true 13 | }, 14 | sleep: { 15 | command: ['pmset', 'sleepnow'], 16 | sudo: false 17 | }, 18 | displaySleep: { 19 | command: ['pmset', 'displaysleepnow'], 20 | sudo: false 21 | }, 22 | logout: { 23 | command: ['osascript', 'src/Commands/AppleScripts/logout.scpt'], 24 | sudo: false 25 | }, 26 | killShutdown: { 27 | command: ['killall', 'shutdown'], 28 | sudo: true 29 | } 30 | } 31 | 32 | var browserCommands = { 33 | googleChromeReset : { 34 | command: ['osascript', 'src/Commands/AppleScripts/browser_googlechrome_reset.scpt'], 35 | sudo: false 36 | }, 37 | safariClearHistory : { 38 | command: ['osascript', 'src/Commands/AppleScripts/browser_safari_clearHistory.scpt'], 39 | sudo: false 40 | } 41 | } 42 | 43 | var wifiCommands = { 44 | on : { 45 | command : ['networksetup', '-setairportpower en0 on'], 46 | sudo: false 47 | }, 48 | off : { 49 | command : ['networksetup', '-setairportpower en0 off'], 50 | sudo: false 51 | }, 52 | status : { 53 | command : ['ifconfig', 'en0'], 54 | sudo: false 55 | }, 56 | connect : { 57 | command : ['networksetup', '-setairportnetwork en0'], 58 | sudo: false 59 | } 60 | } 61 | 62 | var spyCommands = { 63 | screenshot: { 64 | command: ['screencapture', '-x'], 65 | sudo: false 66 | }, 67 | resetiSightCamera: { 68 | command: ['killall', 'VDCAssistant'], 69 | sudo: true 70 | }, 71 | resetCameraAssistant: { 72 | command: ['killall', 'AppleCameraAssistant'], 73 | sudo: true 74 | }, 75 | alert: { 76 | command: ['osascript', 'src/Commands/AppleScripts/showAlert.scpt'], 77 | sudo: false 78 | }, 79 | notify: { 80 | command: ['osascript', 'src/Commands/AppleScripts/notify.scpt'], 81 | sudo: false 82 | }, 83 | camRecord : { 84 | command: ['./src/Commands/SwiftScripts/Recorder/CamRecorder/Recorder start'], 85 | sudo: false 86 | }, 87 | screenRecord : { 88 | command: ['./src/Commands/SwiftScripts/Recorder/ScreenRecorder/Recorder start'], 89 | sudo: false 90 | } 91 | } 92 | 93 | var applicationCommands = { 94 | open: { 95 | command: ['open'], 96 | sudo: false 97 | } 98 | } 99 | 100 | var musicCommands = { 101 | itunesPlaylist: { 102 | command: ['osascript', 'src/Commands/AppleScripts/iTunesPlaylist.scpt'], 103 | sudo: false 104 | }, 105 | setVolume: { 106 | command: ['osascript', 'src/Commands/AppleScripts/setVolume.scpt'], 107 | sudo: false 108 | }, 109 | getVolume: { 110 | command: ['osascript', 'src/Commands/AppleScripts/getVolume.scpt'], 111 | sudo: false 112 | }, 113 | mute: { 114 | command: ['osascript', '-e', '"set volume with output muted"'], 115 | sudo: false 116 | }, 117 | unmute: { 118 | command: ['osascript','-e', '"set volume without output muted"'], 119 | sudo: false 120 | }, 121 | isMute: { 122 | command: ['osascript', 'src/Commands/AppleScripts/isMuted.scpt'], 123 | sudo: false 124 | } 125 | } 126 | 127 | var bluetoothCommands = { 128 | on : { 129 | command : ['blueutil', '-p on'], 130 | sudo: false 131 | }, 132 | off : { 133 | command : ['blueutil', '-p off'], 134 | sudo: false 135 | }, 136 | status : { 137 | command : ['defaults read /Library/Preferences/com.apple.Bluetooth.plist ControllerPowerState'], 138 | sudo: false 139 | }, 140 | showPairingAlert : { 141 | command : ['swift src/Commands/SwiftScripts/showPairing.swift'], 142 | sudo: false 143 | }, 144 | toggle : { 145 | command : ['swift src/Commands/SwiftScripts/toggleBluetooth.swift'], 146 | sudo: false 147 | } 148 | } 149 | 150 | module.exports = { 151 | power: powerCommands, 152 | browser: browserCommands, 153 | spyCommands: spyCommands, 154 | wifi: wifiCommands, 155 | application: applicationCommands, 156 | music: musicCommands, 157 | bluetooth: bluetoothCommands 158 | } -------------------------------------------------------------------------------- /src/Routes/BluetoothRouter.js: -------------------------------------------------------------------------------- 1 | var router = require('express').Router() 2 | var commandService = require('../Services/CommandService.js') 3 | 4 | router.get('/', function (req, res) { 5 | res.send('Bluetooth Router') 6 | }) 7 | 8 | router.post('/on', function (req, res) { 9 | commandService.execute('bluetooth', 'on', null, function() {}) 10 | res.status(200).send({responseMessage: "Bluetooth on"}) 11 | }) 12 | 13 | router.post('/off', function (req, res) { 14 | commandService.execute('bluetooth', 'off', null, function() {}) 15 | res.status(200).send({responseMessage: "Bluetooth off"}) 16 | }) 17 | 18 | router.get('/status', function (req, res) { 19 | commandService.execute('bluetooth', 'status', null, function(response) { 20 | 21 | if (Object.values(response)[0] == 1) { 22 | res.status(200).send({responseMessage: "Bluetooth Status : ON", response: {bluetoothStatus: true}}) 23 | return 24 | } 25 | 26 | res.status(200).send({responseMessage: "Bluetooth Status : OFF", response: {bluetoothStatus: false}}) 27 | }) 28 | }) 29 | 30 | router.post('/showPairingAlert', function (req, res) { 31 | if (req.body.deviceName != null) { 32 | commandService.execute('bluetooth', 'showPairingAlert', [req.body.deviceName], function(data, error) { 33 | if (error) { 34 | res.status(404).send({responseMessage: "No devices found." }) 35 | } else { 36 | res.status(200).send({responseMessage: "Pairing alert presented successfully." }) 37 | } 38 | }) 39 | } else { 40 | res.status(400).send({responseMessage: "Please provide valid bluetooth device name"}) 41 | } 42 | }) 43 | 44 | router.post('/toggle', function (req, res) { 45 | if (req.body.deviceName != null) { 46 | commandService.execute('bluetooth', 'toggle', [req.body.deviceName], function(data, error) { 47 | if (error) { 48 | res.status(404).send({responseMessage: "If failed than please check following scenarios:\ 49 | - Please check bluetooth is enabled or not\ 50 | - Please device is paired or not.\ 51 | - If Paired please check device is discovarable or not" }) 52 | } else { 53 | res.status(200).send({responseMessage: "Connection toggled successfully." }) 54 | } 55 | }) 56 | } else { 57 | res.status(400).send({responseMessage: "Please provide valid bluetooth device name"}) 58 | } 59 | }) 60 | 61 | module.exports = router -------------------------------------------------------------------------------- /src/Routes/BrowserRouter.js: -------------------------------------------------------------------------------- 1 | var router = require('express').Router() 2 | var commandService = require('../Services/CommandService.js') 3 | 4 | router.get('/', function (req, res) { 5 | res.send('Browser Router') 6 | }) 7 | 8 | router.post('/googleChromeReset', function (req, res) { 9 | commandService.execute('browser', 'googleChromeReset', null, function() {}) 10 | res.status(200).send({responseMessage: "Google Chrome Reset"}) 11 | }) 12 | 13 | router.post('/safariClearHistory', function (req, res) { 14 | commandService.execute('browser', 'safariClearHistory', null, function() {}) 15 | res.status(200).send({responseMessage: "Safari Clear History"}) 16 | }) 17 | 18 | 19 | module.exports = router -------------------------------------------------------------------------------- /src/Routes/MusicRouter.js: -------------------------------------------------------------------------------- 1 | var router = require('express').Router() 2 | var commandService = require('../Services/CommandService.js') 3 | var env = require('../environment.js') 4 | const youtubeUrl = 'https://www.youtube.com/watch?v=' 5 | 6 | // define the home page route => /music 7 | router.get('/', function (req, res) { 8 | res.send('Music Router') 9 | }) 10 | 11 | router.post('/youtubePlaylist', function (req, res) { 12 | var options = [] 13 | if (env.youtubePlaylistUrl != null) { 14 | options.push(youtubeUrl + env.youtubePlaylistUrl) 15 | } else { 16 | res.status(400).send({responseMessage: "Youtube Playlist Name missing in environment.js"}) 17 | return 18 | } 19 | 20 | commandService.execute('application', 'open', options, function() { 21 | res.status(200).send({responseMessage: "Youtube Playlist Started"}) 22 | }) 23 | }) 24 | 25 | router.post('/itunesPlaylist', function (req, res) { 26 | var options = [] 27 | if (env.itunesPlaylist != null) { 28 | options.push(env.itunesPlaylist) 29 | } else { 30 | res.status(400).send({responseMessage: "iTunes Playlist Name missing in environment.js"}) 31 | return 32 | } 33 | commandService.execute('music', 'itunesPlaylist', options, function() { 34 | res.status(200).send({responseMessage: "iTunes Playlist Started"}) 35 | }) 36 | }) 37 | 38 | router.post('/setVolume', function (req, res) { 39 | var options = [] 40 | if (req.body.volume != null) { 41 | options.push(req.body.volume) 42 | } else { 43 | res.status(400).send({responseMessage: "Volume Parameter Missing"}) 44 | return 45 | } 46 | commandService.execute('music', 'setVolume', options, function() { 47 | res.status(200).send({responseMessage: "Volume Changed to " + req.body.volume}) 48 | }) 49 | }) 50 | 51 | router.get('/getVolume', function (req, res) { 52 | commandService.execute('music', 'getVolume', null, function(data) { 53 | res.status(200).send({responseMessage: "Volume Status", response: {outputVolume: data[0]}}) 54 | }) 55 | }) 56 | 57 | router.post('/mute', function (req, res) { 58 | commandService.execute('music', 'mute', null, function() { 59 | res.status(200).send({responseMessage: "Muted"}) 60 | }) 61 | }) 62 | 63 | router.post('/unmute', function (req, res) { 64 | commandService.execute('music', 'unmute', null, function() { 65 | res.status(200).send({responseMessage: "UnMuted"}) 66 | }) 67 | }) 68 | 69 | router.get('/isMuted', function (req, res) { 70 | commandService.execute('music', 'isMute', null, function(data) { 71 | res.status(200).send({responseMessage: "Mute Status", response: {outputMuted: data[0]}}) 72 | }) 73 | }) 74 | 75 | module.exports = router 76 | -------------------------------------------------------------------------------- /src/Routes/PowerRouter.js: -------------------------------------------------------------------------------- 1 | var router = require('express').Router() 2 | var commandService = require('../Services/CommandService.js') 3 | 4 | // define the home page route => /power 5 | router.get('/', function (req, res) { 6 | res.send('Power Router') 7 | }) 8 | 9 | router.post('/shutdown', function (req, res) { 10 | var options = [] 11 | if (req.body.time != null) { 12 | options.push('+'+req.body.time) 13 | } else { 14 | options.push('now') 15 | } 16 | 17 | commandService.execute('power', 'shutdown', options, function(){}) 18 | res.status(200).send({responseMessage: "Shutting Down"}) 19 | }) 20 | 21 | router.post('/restart', function (req, res) { 22 | var options = [] 23 | if (req.body.time != null) { 24 | options.push('+'+req.body.time) 25 | } else { 26 | options.push('now') 27 | } 28 | commandService.execute('power', 'restart', options, function() {}) 29 | res.status(200).send({responseMessage: "Restarting"}) 30 | }) 31 | 32 | router.post('/halt', function (req, res) { 33 | commandService.execute('power', 'halt', null, function() {}) 34 | res.status(200).send({responseMessage: "System Halt"}) 35 | }) 36 | 37 | router.post('/logout', function (req, res) { 38 | commandService.execute('power', 'logout', null, function() {}) 39 | res.status(200).send({responseMessage: "Logging out in 60 secs"}) 40 | }) 41 | 42 | router.post('/sleep', function (req, res) { 43 | commandService.execute('power', 'sleep', null, function() {}) 44 | res.status(200).send({responseMessage: "Sleeping"}) 45 | }) 46 | 47 | router.post('/displaySleep', function (req, res) { 48 | commandService.execute('power', 'displaySleep', null, function() {}) 49 | res.status(200).send({responseMessage: "Display Sleep"}) 50 | }) 51 | 52 | router.post('/cancelShutdown', function (req, res) { 53 | commandService.execute('power', 'killShutdown', null, function() {}) 54 | res.status(200).send({responseMessage: "Shutdown/Restart Cancelled"}) 55 | }) 56 | 57 | router.get('/ping', function (req, res) { 58 | res.status(200).send({responseMessage: "Pong"}) 59 | }) 60 | 61 | module.exports = router -------------------------------------------------------------------------------- /src/Routes/SystemSpyRouter.js: -------------------------------------------------------------------------------- 1 | var router = require('express').Router() 2 | var fs = require('fs') 3 | var path = require('path') 4 | var commandService = require('../Services/CommandService.js') 5 | var imagesnapjs = require('imagesnapjs'); 6 | 7 | 8 | const homeDir = require('os').homedir(); 9 | const screenShotDirectory = 'Screenshots_remotecontrol' 10 | const imagesDirectory = path.join(homeDir, screenShotDirectory) 11 | 12 | // define the home page route => /systemSpy 13 | router.get('/', function (req, res) { 14 | res.send('System Spy') 15 | }) 16 | 17 | router.post('/screenshot', function (req, res) { 18 | 19 | if (!fs.existsSync(imagesDirectory)){ 20 | fs.mkdirSync(imagesDirectory); 21 | } 22 | 23 | var imageFilePath = homeDir + '/' + screenShotDirectory + '/' + Date.now().toString() + '.jpg' 24 | var options = [imageFilePath] 25 | 26 | commandService.execute('spyCommands', 'screenshot', options, function(){ 27 | res.status(200).sendFile(imageFilePath) 28 | }) 29 | }) 30 | 31 | router.post('/webcamCapture', function (req, res) { 32 | var imageFilePath = homeDir + '/' + screenShotDirectory + '/' + Date.now().toString() + '.jpg' 33 | var retryCount = 0 34 | 35 | if (!fs.existsSync(imagesDirectory)){ 36 | fs.mkdirSync(imagesDirectory); 37 | } 38 | 39 | var getCameraImage = function() { 40 | retryCount++ 41 | imagesnapjs.capture(imageFilePath, { cliflags: '-w 2'}, function(err) { 42 | if (err != null && retryCount < 3) { 43 | commandService.execute('spyCommands', 'resetiSightCamera', null, function() { 44 | commandService.execute('spyCommands', 'resetCameraAssistant', null, function() { 45 | getCameraImage() 46 | }) 47 | }) 48 | } else { 49 | respondWithImage() 50 | } 51 | }); 52 | } 53 | 54 | var respondWithImage = function() { 55 | res.status(200).sendFile(imageFilePath) 56 | } 57 | 58 | getCameraImage() 59 | 60 | }) 61 | 62 | router.post('/alert', function (req, res) { 63 | var options = [] 64 | if (req.body.message != null) { 65 | options.push(""+req.body.message) 66 | } else { 67 | options.push('Alert') 68 | } 69 | 70 | commandService.execute('spyCommands', 'alert', options, function(){ 71 | res.status(200).send({responseMessage: "Alert Displayed"}) 72 | }) 73 | }) 74 | 75 | router.post('/notify', function (req, res) { 76 | var options = [] 77 | if (req.body.message != null) { 78 | options.push(""+req.body.message) 79 | } else { 80 | options.push('Alert') 81 | } 82 | 83 | if (req.body.title != null) { 84 | options.push(""+req.body.title) 85 | } else { 86 | options.push('Notification') 87 | } 88 | 89 | commandService.execute('spyCommands', 'notify', options, function(){ 90 | res.status(200).send({responseMessage: "Notification Displayed"}) 91 | }) 92 | }) 93 | 94 | router.post('/screenRecord', function (req, res) { 95 | var options = [] 96 | 97 | if (req.body.time != null) { 98 | options.push(""+req.body.time) 99 | } 100 | 101 | commandService.execute('spyCommands', 'screenRecord', options, function(){ 102 | res.status(200).send({responseMessage: "screenRecord Started"}) 103 | }) 104 | }) 105 | 106 | router.post('/camRecord', function (req, res) { 107 | var options = [] 108 | 109 | if (req.body.time != null) { 110 | options.push(""+req.body.time) 111 | } 112 | 113 | commandService.execute('spyCommands', 'camRecord', options, function(){ 114 | res.status(200).send({responseMessage: "camRecord Started"}) 115 | }) 116 | }) 117 | 118 | router.get('/isRecording', function (req, res) { 119 | res.status(404).send({responseMessage: "Unimplemented"}) 120 | }) 121 | 122 | 123 | module.exports = router -------------------------------------------------------------------------------- /src/Routes/SystemStatsRouter.js: -------------------------------------------------------------------------------- 1 | var router = require('express').Router() 2 | const si = require('systeminformation'); 3 | 4 | // define the home page route => /systemStats 5 | router.get('/', function (req, res) { 6 | res.send('System Statistics') 7 | }) 8 | 9 | router.get('/temperature', function (req, res) { 10 | si.cpuTemperature(function(data) { 11 | res.status(200).send({responseMessage: "CPU Temperature", resonse: data}) 12 | }) 13 | }) 14 | 15 | router.get('/cpuLoad', function (req, res) { 16 | si.currentLoad(function(data) { 17 | res.status(200).send({responseMessage: "CPU Stats", resonse: data}) 18 | }) 19 | }) 20 | 21 | router.get('/ram', function (req, res) { 22 | si.mem(function(data) { 23 | res.status(200).send({responseMessage: "Ram Stats", resonse: data}) 24 | }) 25 | }) 26 | 27 | router.get('/storage', function (req, res) { 28 | si.fsSize(function(data) { 29 | res.status(200).send({responseMessage: "Storage Stats", resonse: data}) 30 | }) 31 | }) 32 | 33 | router.get('/battery', function (req, res) { 34 | si.battery(function(data) { 35 | res.status(200).send({responseMessage: "Battery Stats", resonse: data}) 36 | }) 37 | }) 38 | 39 | 40 | module.exports = router -------------------------------------------------------------------------------- /src/Routes/WifiRouter.js: -------------------------------------------------------------------------------- 1 | var router = require('express').Router() 2 | var commandService = require('../Services/CommandService.js') 3 | 4 | router.get('/', function (req, res) { 5 | res.send('Wifi Router') 6 | }) 7 | 8 | router.post('/on', function (req, res) { 9 | commandService.execute('wifi', 'on', null, function() {}) 10 | res.status(200).send({responseMessage: "Wifi on"}) 11 | }) 12 | 13 | router.post('/off', function (req, res) { 14 | commandService.execute('wifi', 'off', null, function() {}) 15 | res.status(200).send({responseMessage: "Wifi off"}) 16 | }) 17 | 18 | router.post('/connect', function (req, res) { 19 | if (req.body.name != null && req.body.password != null) { 20 | commandService.execute('wifi', 'connect', [req.body.name, req.body.password], function(data) { 21 | var responseMessage = "Wifi Connected Successfully" 22 | 23 | if (data[0]) { 24 | responseMessage = data[0] 25 | } 26 | 27 | res.status(200).send({responseMessage: responseMessage}) 28 | }) 29 | } else { 30 | res.status(400).send({responseMessage: "Please provide valid wifi data"}) 31 | } 32 | }) 33 | 34 | router.get('/status', function (req, res) { 35 | commandService.execute('wifi', 'status', null, function(response) { 36 | if (response['\tstatus'] != null && response['\tstatus'] == " active") { 37 | res.status(200).send({responseMessage: "Wifi Status : ON", response: {wifiStatus: true}}) 38 | return 39 | } 40 | 41 | res.status(200).send({responseMessage: "Wifi Status : OFF", response: {wifiStatus: false}}) 42 | }) 43 | }) 44 | 45 | 46 | module.exports = router -------------------------------------------------------------------------------- /src/Services/CommandService.js: -------------------------------------------------------------------------------- 1 | var env = require('../environment.js') 2 | var logger = require('../Utilities/logger.js') 3 | var sudo = require('sudo-js'); 4 | var exec = require('child_process').exec; 5 | var commands = require('../Commands/commands.js') 6 | var logger = require('../Utilities/logger.js') 7 | 8 | function CommandService() { 9 | sudo.setPassword(env.pass) 10 | checkSudoPassword() 11 | } 12 | 13 | function checkSudoPassword() { 14 | sudo.check(function(valid) { 15 | if (!valid) { 16 | logger.error('Invalid Sudo Password. Check environment.js . Some commands wont work properly. Please provide correct sudo password', false) 17 | } 18 | }) 19 | } 20 | 21 | CommandService.prototype.execute = function(commandType, commandKey, params, callback) { 22 | var executableCommand = commands[commandType][commandKey] 23 | 24 | if (executableCommand == null) { 25 | logger.info("Command Not Found : " + commandType + " : " + commandKey) 26 | return 27 | } 28 | 29 | var commandToExecute = executableCommand.command.concat(params) 30 | 31 | logger.debug('*********\nExecuting Command\nSudo: '+executableCommand.sudo + ' \nCommand Type : ' + commandType 32 | + '\nCommand Key : ' + commandKey + '\nCommand : ' + commandToExecute + '\n*********') 33 | 34 | if (params == null) { 35 | params = [] 36 | } 37 | 38 | var processResult = function(stdout) { 39 | var lines = stdout.toString().split('\n'); 40 | var results = new Array(); 41 | lines.forEach(function(line) { 42 | if (line.indexOf(":") !== -1) { 43 | var parts = line.split(':'); 44 | results[parts[0]] = parts[1]; 45 | } else { 46 | results.push(line) 47 | } 48 | }); 49 | 50 | return results 51 | }; 52 | 53 | var callCallback = function(error, stdOut, stdErr) { 54 | logger.error(error, false) 55 | logger.info(stdOut) 56 | if (callback != null) { 57 | callback(processResult(stdOut), error, stdErr) 58 | } 59 | } 60 | 61 | if (executableCommand.sudo) { 62 | sudo.exec(commandToExecute, function(error, pid, result) { 63 | callCallback(error, result, null) 64 | }); 65 | } else { 66 | //Run Shell without sudo 67 | exec(commandToExecute.join(' '), function(error, stdOut, stdErr) { 68 | callCallback(error, stdOut, stdErr) 69 | }) 70 | } 71 | } 72 | 73 | module.exports = new CommandService() -------------------------------------------------------------------------------- /src/Services/RouterService.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var sessionService = require('./SessionService.js') 3 | 4 | function RouterService() { 5 | this.routes = [] 6 | } 7 | 8 | RouterService.prototype.initSessionManager = function(app) { 9 | app.use(function authenticateSessionToken (req, res, next) { 10 | 11 | if (req.headers['token'] == null) { 12 | res.status(400).send({responseMessage: "Authentication Failed: token Header Required.", errorCode: 403}) 13 | return 14 | } 15 | 16 | if (!sessionService.authenticate(req.headers['token'])) { 17 | // Authentication Failed 18 | res.status(400).send({responseMessage: "Error: Authentication Failed", errorCode: 401}) 19 | return 20 | } 21 | 22 | // Continue Routing 23 | next() 24 | }) 25 | app.use(express.json()); // to support JSON-encoded bodies 26 | app.use(express.urlencoded()); 27 | } 28 | //Public Method 29 | RouterService.prototype.addRoutes = function(app, routes) { 30 | this.initSessionManager(app) 31 | this.routes = routes 32 | 33 | // Adding Routes 34 | for (var i=0; i < this.routes.length; i++) { 35 | var route = this.routes[i] 36 | app.use(route.url, route.routerClass) 37 | } 38 | } 39 | 40 | module.exports = new RouterService() -------------------------------------------------------------------------------- /src/Services/SessionService.js: -------------------------------------------------------------------------------- 1 | var env = require('../environment.js') 2 | 3 | function SessionService() { 4 | this.sessionToken = env.sessionToken 5 | } 6 | 7 | SessionService.prototype.authenticate = function(sessionToken) { 8 | return (sessionToken == this.sessionToken) 9 | } 10 | 11 | module.exports = new SessionService() -------------------------------------------------------------------------------- /src/Utilities/logger.js: -------------------------------------------------------------------------------- 1 | var env = require('../environment.js') 2 | 3 | function Logger() { 4 | 5 | } 6 | 7 | Logger.prototype.error = function(log, shouldExit = true) { 8 | if (log != null) { 9 | console.log("Error : ") 10 | console.log(log) 11 | } 12 | if (shouldExit) { 13 | process.exit() 14 | } 15 | } 16 | 17 | Logger.prototype.info = function(log) { 18 | if (env.logLevel == 'info') { 19 | console.log(log) 20 | } 21 | } 22 | 23 | Logger.prototype.debug = function(log) { 24 | if (env.logLevel == 'debug' || env.logLevel == 'info') { 25 | console.log(log) 26 | } 27 | } 28 | 29 | Logger.prototype.log = function(log) { 30 | console.log(log) 31 | } 32 | 33 | module.exports = new Logger() -------------------------------------------------------------------------------- /src/environment.js: -------------------------------------------------------------------------------- 1 | var env = { 2 | port: '3000', 3 | pass: 'uncharted', 4 | logLevel: 'info', 5 | sessionToken: 'f64f2940-fae4-11e7-8c5f-ef356f279131', 6 | youtubePlaylistUrl: 'qpgTC9MDx1o&list=RDMMqpgTC9MDx1o', 7 | itunesPlaylist: 'MyPlaylist' 8 | } 9 | 10 | module.exports = env 11 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | var powerRouter = require('./Routes/PowerRouter.js') 2 | var systemStatsRouter = require('./Routes/SystemStatsRouter.js') 3 | var browserRouter = require('./Routes/BrowserRouter.js') 4 | var systemSpyRouter = require('./Routes/SystemSpyRouter.js') 5 | var wifiRouter = require('./Routes/WifiRouter.js') 6 | var musicRouter = require('./Routes/MusicRouter.js') 7 | var bluetoothRouter = require('./Routes/BluetoothRouter.js') 8 | 9 | var routes = [ 10 | { 11 | url: '/power', 12 | routerClass: powerRouter 13 | }, 14 | { 15 | url: '/systemStats', 16 | routerClass: systemStatsRouter 17 | }, 18 | { 19 | url: '/browser', 20 | routerClass: browserRouter 21 | }, 22 | { 23 | url: '/systemSpy', 24 | routerClass: systemSpyRouter 25 | }, 26 | { 27 | url: '/wifi', 28 | routerClass: wifiRouter 29 | }, 30 | { 31 | url: '/music', 32 | routerClass: musicRouter 33 | }, 34 | { 35 | url: '/bluetooth', 36 | routerClass: bluetoothRouter 37 | } 38 | ] 39 | 40 | module.exports = routes -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var routes = require('./routes.js') 4 | var routerService = require('./Services/RouterService.js') 5 | var sessionService = require('./Services/SessionService.js') 6 | var commandService = require('./Services/CommandService.js') 7 | var mylocalip = require('my-local-ip') 8 | var app = require('express')() 9 | var env = require('./environment.js') 10 | var logger = require('./Utilities/logger.js') 11 | 12 | if (process.argv[2] == "--config") { 13 | logger.log("Opening Config ...") 14 | var configLocation = __dirname + "/environment.js" 15 | logger.log("Configuration File Location : " + configLocation) 16 | commandService.execute('application', 'open', [configLocation], function() { 17 | process.exit() 18 | }) 19 | return 20 | } 21 | 22 | logger.info("Starting Server ...") 23 | //Add Routes to app 24 | routerService.addRoutes(app, routes) 25 | 26 | app.listen(env.port, () => logger.log('Remote Control Server Running at ' + mylocalip() + ':' + env.port + '. \nSession Token = ' + sessionService.sessionToken)) -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const express = require('express'); 3 | 4 | var routerService = require('./Services/RouterService.js') 5 | var routes = require('./routes.js') 6 | 7 | 8 | var app = express(); 9 | 10 | routerService.addRoutes(app, routes) 11 | 12 | request(app) 13 | .get("/power") 14 | .set('token', 'f64f2940-fae4-11e7-8c5f-ef356f279131') 15 | .expect(200) 16 | .end(function(err) { 17 | if (err) { 18 | throw err; 19 | } else { 20 | console.log("Test 1 Passed") 21 | } 22 | }); 23 | 24 | request(app) 25 | .get("/power") 26 | .expect(400) 27 | .end(function(err) { 28 | if (err) { 29 | throw err; 30 | } else { 31 | console.log("Test 2 Passed") 32 | } 33 | }); 34 | 35 | request(app) 36 | .get("/power/ping") 37 | .set('token', 'f64f2940-fae4-11e7-8c5f-ef356f279131') 38 | .expect(200) 39 | .expect(function(res) { 40 | res.body.responseMessage = 'Pong'; 41 | }) 42 | .end(function(err) { 43 | if (err) { 44 | throw err; 45 | } else { 46 | console.log("Test 3 Passed") 47 | } 48 | }); 49 | --------------------------------------------------------------------------------