Skip to content

Commit 224f63c

Browse files
JoshM1994Shahen Hovhannisyan
authored andcommitted
feat(add reversal): ios (shahen94#157)
* adding reverse to process manager on ios * ext react method for reverse * adding credit * removing excess * removing more rubbish * adding boomerang and reverse * adding reverse and boomerang functions in swift * adding credit * adding credit * fixing wrong c&p * adding boomerang to bridge * calling back error rather than just printing * removing logs and commented code
1 parent 402705b commit 224f63c

File tree

4 files changed

+235
-0
lines changed

4 files changed

+235
-0
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Credit to whydna and yayoc
3+
* https://github.com/whydna/Reverse-AVAsset-Efficient
4+
*/
5+
6+
import UIKit
7+
import AVFoundation
8+
9+
class AVUtilities {
10+
static func reverse(_ original: AVAsset, outputURL: URL, completion: @escaping (AVAsset) -> Void) {
11+
12+
// Initialize the reader
13+
14+
var reader: AVAssetReader! = nil
15+
do {
16+
reader = try AVAssetReader(asset: original)
17+
} catch {
18+
print("could not initialize reader.")
19+
return
20+
}
21+
22+
guard let videoTrack = original.tracks(withMediaType: AVMediaTypeVideo).last else {
23+
print("could not retrieve the video track.")
24+
return
25+
}
26+
27+
let readerOutputSettings: [String: Any] = [kCVPixelBufferPixelFormatTypeKey as String : Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)]
28+
let readerOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: readerOutputSettings)
29+
reader.add(readerOutput)
30+
31+
reader.startReading()
32+
33+
// read in samples
34+
35+
var samples: [CMSampleBuffer] = []
36+
while let sample = readerOutput.copyNextSampleBuffer() {
37+
samples.append(sample)
38+
}
39+
40+
// Initialize the writer
41+
42+
let writer: AVAssetWriter
43+
do {
44+
writer = try AVAssetWriter(outputURL: outputURL, fileType: AVFileTypeQuickTimeMovie)
45+
} catch let error {
46+
fatalError(error.localizedDescription)
47+
}
48+
49+
let videoCompositionProps = [AVVideoAverageBitRateKey: videoTrack.estimatedDataRate]
50+
let writerOutputSettings = [
51+
AVVideoCodecKey: AVVideoCodecH264,
52+
AVVideoWidthKey: videoTrack.naturalSize.width,
53+
AVVideoHeightKey: videoTrack.naturalSize.height,
54+
AVVideoCompressionPropertiesKey: videoCompositionProps
55+
] as [String : Any]
56+
57+
let writerInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: writerOutputSettings)
58+
writerInput.expectsMediaDataInRealTime = false
59+
writerInput.transform = videoTrack.preferredTransform
60+
61+
let pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: writerInput, sourcePixelBufferAttributes: nil)
62+
63+
writer.add(writerInput)
64+
writer.startWriting()
65+
writer.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(samples.first!))
66+
67+
for (index, sample) in samples.enumerated() {
68+
let presentationTime = CMSampleBufferGetPresentationTimeStamp(sample)
69+
let imageBufferRef = CMSampleBufferGetImageBuffer(samples[samples.count - 1 - index])
70+
while !writerInput.isReadyForMoreMediaData {
71+
Thread.sleep(forTimeInterval: 0.1)
72+
}
73+
pixelBufferAdaptor.append(imageBufferRef!, withPresentationTime: presentationTime)
74+
75+
}
76+
77+
writer.finishWriting {
78+
completion(AVAsset(url: outputURL))
79+
}
80+
}
81+
}
82+
83+

ios/RNVideoProcessing/RNVideoTrimmer/RNVideoTrimmer.swift

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,132 @@ class RNVideoTrimmer: NSObject {
252252
}
253253
}
254254
}
255+
256+
@objc func boomerang(_ source: String, options: NSDictionary, callback: @escaping RCTResponseSenderBlock) {
257+
258+
let quality = ""
259+
260+
let manager = FileManager.default
261+
guard let documentDirectory = try? manager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
262+
else {
263+
callback(["Error creating FileManager", NSNull()])
264+
return
265+
}
266+
267+
let sourceURL = getSourceURL(source: source)
268+
let firstAsset = AVAsset(url: sourceURL as URL)
269+
270+
let mixComposition = AVMutableComposition()
271+
let track = mixComposition.addMutableTrack(withMediaType: AVMediaTypeVideo, preferredTrackID: Int32(kCMPersistentTrackID_Invalid))
272+
273+
274+
var outputURL = documentDirectory.appendingPathComponent("output")
275+
var finalURL = documentDirectory.appendingPathComponent("output")
276+
do {
277+
try manager.createDirectory(at: outputURL, withIntermediateDirectories: true, attributes: nil)
278+
try manager.createDirectory(at: finalURL, withIntermediateDirectories: true, attributes: nil)
279+
let name = randomString()
280+
outputURL = outputURL.appendingPathComponent("\(name).mp4")
281+
finalURL = finalURL.appendingPathComponent("\(name)merged.mp4")
282+
} catch {
283+
callback([error.localizedDescription, NSNull()])
284+
print(error)
285+
}
286+
287+
//Remove existing file
288+
_ = try? manager.removeItem(at: outputURL)
289+
_ = try? manager.removeItem(at: finalURL)
290+
291+
let useQuality = getQualityForAsset(quality: quality, asset: firstAsset)
292+
293+
// print("RNVideoTrimmer passed quality: \(quality). useQuality: \(useQuality)")
294+
295+
AVUtilities.reverse(firstAsset, outputURL: outputURL, completion: { [unowned self] (reversedAsset: AVAsset) in
296+
297+
298+
let secondAsset = reversedAsset
299+
300+
// Credit: https://www.raywenderlich.com/94404/play-record-merge-videos-ios-swift
301+
do {
302+
try track.insertTimeRange(CMTimeRangeMake(kCMTimeZero, firstAsset.duration), of: firstAsset.tracks(withMediaType: AVMediaTypeVideo)[0], at: kCMTimeZero)
303+
} catch _ {
304+
callback( ["Failed: Could not load 1st track", NSNull()] )
305+
return
306+
}
307+
308+
do {
309+
try track.insertTimeRange(CMTimeRangeMake(kCMTimeZero, secondAsset.duration), of: secondAsset.tracks(withMediaType: AVMediaTypeVideo)[0], at: mixComposition.duration)
310+
} catch _ {
311+
callback( ["Failed: Could not load 2nd track", NSNull()] )
312+
return
313+
}
314+
315+
316+
guard let exportSession = AVAssetExportSession(asset: mixComposition, presetName: useQuality) else {
317+
callback(["Error creating AVAssetExportSession", NSNull()])
318+
return
319+
}
320+
exportSession.outputURL = NSURL.fileURL(withPath: finalURL.path)
321+
exportSession.outputFileType = AVFileTypeMPEG4
322+
exportSession.shouldOptimizeForNetworkUse = true
323+
let startTime = CMTime(seconds: Double(0), preferredTimescale: 1000)
324+
let endTime = CMTime(seconds: mixComposition.duration.seconds, preferredTimescale: 1000)
325+
let timeRange = CMTimeRange(start: startTime, end: endTime)
326+
327+
exportSession.timeRange = timeRange
328+
329+
exportSession.exportAsynchronously{
330+
switch exportSession.status {
331+
case .completed:
332+
callback( [NSNull(), finalURL.absoluteString] )
333+
334+
case .failed:
335+
callback( ["Failed: \(exportSession.error)", NSNull()] )
336+
337+
case .cancelled:
338+
callback( ["Cancelled: \(exportSession.error)", NSNull()] )
339+
340+
default: break
341+
}
342+
}
343+
})
344+
}
345+
346+
@objc func reverse(_ source: String, options: NSDictionary, callback: @escaping RCTResponseSenderBlock) {
347+
348+
let quality = ""
349+
350+
let manager = FileManager.default
351+
guard let documentDirectory = try? manager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
352+
else {
353+
callback(["Error creating FileManager", NSNull()])
354+
return
355+
}
356+
357+
let sourceURL = getSourceURL(source: source)
358+
let asset = AVAsset(url: sourceURL as URL)
359+
360+
var outputURL = documentDirectory.appendingPathComponent("output")
361+
do {
362+
try manager.createDirectory(at: outputURL, withIntermediateDirectories: true, attributes: nil)
363+
let name = randomString()
364+
outputURL = outputURL.appendingPathComponent("\(name).mp4")
365+
} catch {
366+
callback([error.localizedDescription, NSNull()])
367+
print(error)
368+
}
369+
370+
//Remove existing file
371+
_ = try? manager.removeItem(at: outputURL)
372+
373+
let useQuality = getQualityForAsset(quality: quality, asset: asset)
374+
375+
print("RNVideoTrimmer passed quality: \(quality). useQuality: \(useQuality)")
376+
377+
AVUtilities.reverse(asset, outputURL: outputURL, completion: { [unowned self] (asset: AVAsset) in
378+
callback( [NSNull(), outputURL.absoluteString] )
379+
})
380+
}
255381

256382
@objc func compress(_ source: String, options: NSDictionary, callback: @escaping RCTResponseSenderBlock) {
257383

ios/RNVideoProcessing/RNVideoTrimmer/RNVideoTrimmerBridge.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ @interface RCT_EXTERN_MODULE(RNVideoTrimmer, NSObject)
1111

1212
RCT_EXTERN_METHOD(getAssetInfo:(NSString *)source callback:(RCTResponseSenderBlock)callback);
1313
RCT_EXTERN_METHOD(trim:(NSString *)source options:(NSDictionary *)options callback:(RCTResponseSenderBlock)callback);
14+
RCT_EXTERN_METHOD(reverse:(NSString *)source options:(NSDictionary *)options callback:(RCTResponseSenderBlock)callback);
15+
RCT_EXTERN_METHOD(boomerang:(NSString *)source options:(NSDictionary *)options callback:(RCTResponseSenderBlock)callback);
1416
RCT_EXTERN_METHOD(compress:(NSString *)source options:(NSDictionary *)options callback:(RCTResponseSenderBlock)callback);
1517
RCT_EXTERN_METHOD(getPreviewImageAtPosition:(NSString *)source atTime:(float *)atTime maximumSize:(NSDictionary *)maximumSize format:(NSString *)format callback:(RCTResponseSenderBlock)callback);
1618
RCT_EXTERN_METHOD(crop:(NSString *)source options:(NSDictionary *)options callback:(RCTResponseSenderBlock)callback);

lib/ProcessingManager/ProcessingManager.ios.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,30 @@ export class ProcessingManager {
2525
});
2626
}
2727

28+
static reverse(source: sourceType, options: trimOptions = {}): Promise<string> {
29+
const actualSource: string = getActualSource(source);
30+
return new Promise((resolve, reject) => {
31+
RNVideoTrimmer.reverse(actualSource, options, (err: Object<*>, output: string) => {
32+
if (err) {
33+
return reject(err);
34+
}
35+
return resolve(output);
36+
});
37+
});
38+
}
39+
40+
static boomerang(source: sourceType, options: trimOptions = {}): Promise<string> {
41+
const actualSource: string = getActualSource(source);
42+
return new Promise((resolve, reject) => {
43+
RNVideoTrimmer.boomerang(actualSource, options, (err: Object<*>, output: string) => {
44+
if (err) {
45+
return reject(err);
46+
}
47+
return resolve(output);
48+
});
49+
});
50+
}
51+
2852
static getPreviewForSecond(
2953
source: sourceType,
3054
forSecond: ?number = 0,

0 commit comments

Comments
 (0)