{"id":13566459,"url":"https://github.com/hansemannn/titanium-replaykit","last_synced_at":"2025-04-15T20:43:22.230Z","repository":{"id":142791359,"uuid":"123167153","full_name":"hansemannn/titanium-replaykit","owner":"hansemannn","description":"Use the iOS ReplayKit API's to record the app screen in Titanium and Hyperloop.","archived":false,"fork":false,"pushed_at":"2018-03-16T11:57:20.000Z","size":39,"stargazers_count":12,"open_issues_count":0,"forks_count":4,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-29T01:11:57.319Z","etag":null,"topics":["appcelerator","ios","javascript","native","replaykit","titanium"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hansemannn.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2018-02-27T17:55:45.000Z","updated_at":"2025-03-18T10:15:39.000Z","dependencies_parsed_at":"2023-03-18T06:01:03.015Z","dependency_job_id":null,"html_url":"https://github.com/hansemannn/titanium-replaykit","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hansemannn%2Ftitanium-replaykit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hansemannn%2Ftitanium-replaykit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hansemannn%2Ftitanium-replaykit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hansemannn%2Ftitanium-replaykit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hansemannn","download_url":"https://codeload.github.com/hansemannn/titanium-replaykit/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249151932,"owners_count":21221152,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["appcelerator","ios","javascript","native","replaykit","titanium"],"created_at":"2024-08-01T13:02:10.218Z","updated_at":"2025-04-15T20:43:22.197Z","avatar_url":"https://github.com/hansemannn.png","language":null,"funding_links":[],"categories":["Others"],"sub_categories":[],"readme":"# iOS 11+ ReplayKit API\n\nRecord or stream video from the screen, and audio from the app and microphone \nin Titanium and Hyperloop.\n\nUsing the [ReplayKit](https://developer.apple.com/documentation/replaykit) framework, users can record video from the screen, and audio \nfrom the app and microphone. They can then share their recordings with other users \nthrough email, messages, and social media. You can build app extensions for live \nbroadcasting your content to sharing services.\n\n## Requirements\n\n- [x] iOS 11+\n- [x] Titanium SDK 7.1.0\n- [x] Hyperloop 3.1.0\n- [x] An iOS device (I was not able to run the native API's on the Simulator)\n\n(Alternatively, you can use `require()` statements with SDK 7.0.2 and Hyperloop 3.0.2)\n\n## Usage\n\n1. Create a new file `ti.screenrecorder.js` in your project\n2. Paste the contents of the \"TiScreenRecorder\" class in it\n3. Include the following snippet into your code\n```js\nimport TiScreenRecorder from 'ti.screenrecorder';\n\nconst screenRecorder = new TiScreenRecorder({\n  filePath: `${Ti.Filesystem.applicationDataDirectory}recording-${(new Date()).getTime()}.mp4`,\n  callback: (recording, err) =\u003e {\n    if (err) {\n      alert(`Error recording screen: ${err}`)\n      return;\n    }\n    const message = `Success! See recording in ${recording}`\n    alert(message);\n  }\n});\n\n// Start\n// screenRecorder.startCapture();\n\n// Stop\n// screenRecorder.stopCapture();\n```\n4. Thats it! You can now capture the screen and mic-audio!\n\n(App-audio is not supported so far, but can be done the same way than the mic-audio ~ via `AVAssetWriterInput`)\n\n## Example\n\n```js\nimport {\n  RPScreenRecorder,\n  ReplayKit\n} from 'ReplayKit';\n\nimport {\n  AVAssetWriter,\n  AVAssetWriterInput,\n  AVCaptureDevice,\n  AVFoundation\n} from 'AVFoundation';\n\nimport { NSURL, NSError } from 'Foundation';\n\nimport { UIScreen } from 'UIKit';\n\nimport { CoreMedia } from 'CoreMedia';\n\nconst AVVideoCodecKey = AVFoundation.AVVideoCodecKey;\nconst AVVideoWidthKey = AVFoundation.AVVideoWidthKey;\nconst AVVideoHeightKey = AVFoundation.AVVideoHeightKey;\nconst AVVideoCompressionPropertiesKey = AVFoundation.AVVideoCompressionPropertiesKey;\n\nconst AVVideoPixelAspectRatioKey = AVFoundation.AVVideoPixelAspectRatioKey;\nconst AVVideoCleanApertureKey = AVFoundation.AVVideoCleanApertureKey;\nconst AVVideoCleanApertureWidthKey = AVFoundation.AVVideoCleanApertureWidthKey;\nconst AVVideoCleanApertureHeightKey = AVFoundation.AVVideoCleanApertureHeightKey;\nconst AVVideoCleanApertureVerticalOffsetKey = AVFoundation.AVVideoCleanApertureVerticalOffsetKey;\nconst AVVideoCleanApertureHorizontalOffsetKey = AVFoundation.AVVideoCleanApertureHorizontalOffsetKey;\nconst AVVideoPixelAspectRatioVerticalSpacingKey = AVFoundation.AVVideoPixelAspectRatioVerticalSpacingKey;\nconst AVVideoPixelAspectRatioHorizontalSpacingKey = AVFoundation.AVVideoPixelAspectRatioHorizontalSpacingKey;\n\nconst AVFormatIDKey = AVFoundation.AVFormatIDKey;\nconst AVNumberOfChannelsKey = AVFoundation.AVNumberOfChannelsKey;\nconst AVSampleRateKey = AVFoundation.AVSampleRateKey;\nconst AVEncoderBitRateKey = AVFoundation.AVEncoderBitRateKey;\n\nexport default class TiScreenRecorder {\n  constructor(args = {}) {\n    const filePath = args.filePath;\n    const callback = args.callback;\n\n    if (!filePath) {\n      Ti.API.error('Missing `filePath` property!');\n      return;\n    }\n\n    if (!callback) {\n      Ti.API.error('Missing `callback` property!');\n      return;\n    }\n\n    this.filePath = filePath;\n    this.callback = callback;\n\n    this.isMicEnabled = false;\n    this.videoSessionStarted = false;\n    this.micSessionStarted = false;\n  }\n  startCapture() {\n    if (!RPScreenRecorder.sharedRecorder().isAvailable()) {\n      callback(null, 'Screen Recorder is not available!');\n      return;\n    }\n\n    // Prepare asset writer\n    const fileURL = NSURL.fileURLWithPath(this.filePath);\n    const screenSize = UIScreen.mainScreen.bounds.size;\n\n    Ti.API.info(`Recording video to ${fileURL.absoluteString} ...`);\n\n    this.assetWriter = AVAssetWriter.assetWriterWithURLFileTypeError(fileURL, AVFoundation.AVFileTypeMPEG4, null); // TODO: Handle error?\n    this.assetWriter.movieTimeScale = 60;\n\n    // Video input\n    const videoInputSettings = {\n      // AVVideoCompressionPropertiesKey: {\n      //   AVVideoPixelAspectRatioKey: {\n      //     AVVideoPixelAspectRatioVerticalSpacingKey: 1,\n      //     AVVideoPixelAspectRatioHorizontalSpacingKey: 1\n      //   },\n      //   AVVideoCleanApertureKey: {\n      //     AVVideoCleanApertureWidthKey: screenSize.width,\n      //     AVVideoCleanApertureHeightKey: screenSize.height,\n      //     AVVideoCleanApertureVerticalOffsetKey: 10,\n      //     AVVideoCleanApertureHorizontalOffsetKey: 10\n      //   }\n      // },\n      AVVideoCodecKey: AVFoundation.AVVideoCodecTypeH264,\n      AVVideoWidthKey: screenSize.width,\n      AVVideoHeightKey: screenSize.height\n    };\n\n    this.videoInput = AVAssetWriterInput.assetWriterInputWithMediaTypeOutputSettings(AVFoundation.AVMediaTypeVideo, videoInputSettings);\n    this.videoInput.expectsMediaDataInRealTime = true;\n    this.videoInput.mediaTimeScale = 60\n    if (this.assetWriter.canAddInput(this.videoInput)) {\n      this.assetWriter.addInput(this.videoInput);\n    } else {\n      this.callback(null, 'Cannot add video input');\n      return;\n    }\n\n    // Enable mic if selected\n    if (this.isMicEnabled) {\n      const audioInputSettings = {\n        AVFormatIDKey: 1633772320,\n        AVNumberOfChannelsKey: 1,\n        AVSampleRateKey: 44100.0,\n        AVEncoderBitRateKey: 64000\n      };\n\n      this.audioInput = AVAssetWriterInput.assetWriterInputWithMediaTypeOutputSettings(AVFoundation.AVMediaTypeAudio, audioInputSettings);\n      this.audioInput.expectsMediaDataInRealTime = true;\n\n      if (this.assetWriter.canAddInput(this.videoInput)) {\n        this.assetWriter.addInput(this.audioInput);\n        RPScreenRecorder.sharedRecorder().microphoneEnabled = true;\n      } else {\n        this.callback(null, 'Cannot add audio input');\n        return;\n      }\n    }\n\n    // Start screen capture\n    RPScreenRecorder.sharedRecorder().startCaptureWithHandlerCompletionHandler((sample, bufferType, error) =\u003e {\n      if (error !== null) {\n        Ti.API.error('Error in startCapture: ' + error.localizedDescription);\n        this.callback(null, error.localizedDescription);\n        return;\n      }\n\n      if (!CoreMedia.CMSampleBufferDataIsReady(sample)) {\n        Ti.API.warn('Not ready for writing ...');\n        return;\n      }\n\n      if (this.assetWriter.status === AVFoundation.AVAssetWriterStatusFailed) {\n        Ti.API.error(`Error handling writer: ${this.assetWriter.error.localizedDescription}`);\n        this.callback(null, assetWriter.error.localizedDescription);\n        return;\n      }\n\n      if (this.assetWriter.status !== AVFoundation.AVAssetWriterStatusWriting) {\n        Ti.API.debug(`Not writing ... Status = ${this.assetWriter.status}`);\n      }\n\n      switch (bufferType) {\n        case ReplayKit.RPSampleBufferTypeVideo:\n          if (!this.videoSessionStarted) {\n            Ti.API.info('Starting video session ...');\n            this.videoSessionStarted = true;\n            if (!this.assetWriter.startWriting()) {\n              this.callback(null, this.assetWriter.error.localizedDescription);\n              return;\n            }\n            this.assetWriter.startSessionAtSourceTime(CMSampleBufferGetPresentationTimeStamp(sample));\n          }\n\n          if (this.videoInput.isReadyForMoreMediaData) {\n            Ti.API.info('Adding buffer to video input ...');\n            this.videoInput.appendSampleBuffer(sampleBuffer);\n          }\n          break;\n        case ReplayKit.RPSampleBufferTypeAudioMic:\n          if (!this.isMicEnabled) {\n            Ti.API.warn('Received audio buffer, but mic not enabled. Skipping ...');\n            break;\n          }\n\n          if (!this.micSessionStarted) {\n            Ti.API.info('Starting audio session ...');\n            this.micSessionStarted = true;\n            this.assetWriter.startSessionAtSourceTime(CMSampleBufferGetPresentationTimeStamp(sample));\n          }\n\n          if (this.audioInput.isReadyForMoreMediaData) {\n            // Ti.API.info('Adding buffer to audio input ...');\n            this.audioInput.appendSampleBuffer(sampleBuffer);\n          }\n          break;\n\n        default:\n          break;\n      }\n    }, error =\u003e {\n      if (!error) { return; }\n      Ti.API.error(`Error recording screen: ${error.localizedDescription}, Code = ${error.code}`);\n      this.callback(null, error.localizedDescription);\n    });\n  }\n\n  stopCapture() {\n    RPScreenRecorder.sharedRecorder().stopCaptureWithHandler(error =\u003e {\n      if (!error \u0026\u0026 this.assetWriter.status !== AVFoundation.AVAssetWriterStatusFailed) {\n        this.assetWriter.finishWritingWithCompletionHandler(() =\u003e {\n          Ti.API.info('Status: ' + this._assetWriterStatusToString(this.assetWriter.status));\n          if (this.assetWriter.status !== AVFoundation.AVAssetWriterStatusCompleted) {\n            Ti.API.error('Error finishing writing ...');\n            this.callback(null, this.assetWriter.error.localizedDescription);\n            return;\n          }\n          this.callback(this.assetWriter.outputURL.absoluteString, null);\n          this.assetWriter = null;\n        });\n      } else {\n        Ti.API.error(`Error stopping capture: ${error.localizedDescription}`);\n        this.callback(null, error.localizedDescription);\n      }\n    });\n  }\n\n  requestAudioPermissions(block) {\n    AVCaptureDevice.requestAccessForMediaTypeCompletionHandler(AVFoundation.AVMediaTypeAudio, granted =\u003e {\n      this.isMicEnabled = granted;\n      block(granted);\n    });\n  }\n\n  /* unused */\n  requestVideoPermissions(block) {\n    AVCaptureDevice.requestAccessForMediaTypeCompletionHandler(AVFoundation.AVMediaTypeVideo, granted =\u003e {\n      block(granted);\n    });\n  }\n\n  _assetWriterStatusToString(status) {\n    switch (status) {\n      case AVFoundation.AVAssetWriterStatusUnknown:\n        return 'Unknown';\n      case AVFoundation.AVAssetWriterStatusWriting:\n        return 'Writing';\n      case AVFoundation.AVAssetWriterStatusCompleted:\n        return 'Completed';\n      case AVFoundation.AVAssetWriterStatusFailed:\n        return 'Failed';\n      case AVFoundation.AVAssetWriterStatusCancelled:\n        return 'Cancelled';\n    }\n\n    return '(Unhandled)';\n  }\n}\n```\n\n## Known issues\n\n- The video size seems to be 0 bytes on iOS 11.3, I have no idea why so far.\n\n## License\n\nMIT\n\n## Author\n\nHans Knöchel ([@hansemannnn](https://twitter.com/hansemannnn) / [Web](http://hans-knoechel.de))\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhansemannn%2Ftitanium-replaykit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhansemannn%2Ftitanium-replaykit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhansemannn%2Ftitanium-replaykit/lists"}