CMTime Segment Finder (binary search) - kirseia/study GitHub Wiki

ํŠน์ • ์‹œ๊ฐ„์— ํฌํ•จ๋˜๋Š” ์•„์ดํ…œ์„ ๋ชจ๋‘ ์ฐพ๊ธฐ

  • ๋‹จ, ์•„์ดํ…œ์€ sorted CMTimeRange Array ๋ผ๊ณ  ๊ฐ€์ •ํ•œ๋‹ค.

Source Ver.2

  • ์•„๋ž˜ ver.1 ์— ๋ฌธ์ œ๊ฐ€ ์žˆ์–ด์„œ ์ˆ˜์ •ํ–ˆ์Œ
        let timeRange1 = CMTimeRange(start: .zero, end: 2.5.time)
        let timeRange8 = CMTimeRange(start: 2.5.time, end: 10.0.time)
        let timeRange2 = CMTimeRange(start: 3.0.time, end: 5.0.time)
        let timeRange3 = CMTimeRange(start: 4.0.time, end: 5.5.time)
        let timeRange4 = CMTimeRange(start: 4.5.time, end: 7.0.time)
        let timeRange5 = CMTimeRange(start: 7.0.time, end: 7.5.time)
        let timeRange6 = CMTimeRange(start: 7.7.time, end: 8.0.time)
        let timeRange7 = CMTimeRange(start: 9.0.time, end: 15.0.time)

์ด๋Ÿฐ์‹์œผ๋กœ ์žˆ๋‹ค๋ฉด sort ๊ฐ€ start ๊ธฐ์ค€์œผ๋กœ๋งŒ ๋˜์–ด์žˆ๋‹ค๊ณ  ํ•  ๋•Œ... end ๋ฅผ ์ฐพ์„ ๋•Œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ๋”ฐ๋ผ์„œ binary search ๋กœ lower/upper bound ๋ฅผ ์ฐพ๋Š”๊ฑด ๋ฌธ์ œ๊ฐ€ ๋จ.

๊ทธ๋ž˜์„œ binary search ๋Š” ํ•œ์ชฝ๋งŒ ์ฐพ๋Š” ๊ฒƒ์œผ๋กœ ์ฒ˜๋ฆฌํ–ˆ๋‹ค. ๋Œ€์‹ , ์ „์ฒด ์‹œ๊ฐ„ ๊ธฐ์ค€์œผ๋กœ ์•ž์ชฝ์ด๋ฉด ์•ž๋ถ€ํ„ฐ, ๋’ค ์ชฝ์ด๋ฉด ๋’ค๋ถ€ํ„ฐ ์ฐพ๋Š” ๋ฐฉ์‹์„ ๋„์ž…ํ–ˆ์Œ.

import AVFoundation

extension Double {
    var time: CMTime {
        return CMTime(seconds: self, preferredTimescale: 1000)
    }
}

extension CMTime {
    public func isNearlyEqualTo(time: CMTime, _ tolerance: CMTime = CMTime(value: 1, timescale: 600)) -> Bool {
        let delta = CMTimeAbsoluteValue(self - time)
        return delta < tolerance
    }
    
}

protocol TimeRange {
    var range: CMTimeRange { get set }
}

// ๋น„๊ต์šฉ
class TimeRangeSearch<T> where T: TimeRange {
    var array: [T] = []
    
    func list(on time: CMTime) -> [T] {
        return array.filter { $0.range.containsTime(time) }
    }
}

// FastTimeRangeSearch ๊ฐ€ ์•ฝ 30%์ •๋„ ๋น ๋ฆ„
// (๋‹จ์ผ ๊ฑด์œผ๋กœ๋Š” ์—„์ฒญ๋‚œ ์ฐจ์ด๋Š” ๋‚˜์ง€ ์•Š์œผ๋‚˜ ๋งค frame ๋งˆ๋‹ค ํ˜ธ์ถœ๋ ํ…Œ๋‹ˆ ํšจ๊ณผ๋Š” ์žˆ์„ ๊ฒƒ์œผ๋กœ ๋ณด์ž„)
class FastTimeRangeSearch<T> where T: TimeRange {
    var array: [T] = [] {
        didSet {
            startSortArray = array.sorted { (lhs, rhs) -> Bool in
                return lhs.range.start < rhs.range.start
            }
            
            startTime = startSortArray.first?.range.start ?? .zero
            
            endSortArray = array.sorted(by: { (lhs, rhs) -> Bool in
                return lhs.range.end < rhs.range.end
            })
            
            endTime = endSortArray.last?.range.end ?? .zero
        }
    }
    
    init() {
        
    }
    
    init(array: [T]) {
        self.array = array
    }
    
    private var startSortArray: [T] = []
    private var endSortArray: [T] = []
    
    private var startTime: CMTime = .zero
    private var endTime: CMTime = .zero
    
    func list(on time: CMTime) -> [T] {
        // ์–‘๋ฐฉํ–ฅ์œผ๋กœ binarySearch ๋Š” ๋ถˆ๊ฐ€ํ•ด์„œ ๊ทธ๋‚˜๋งˆ ๋ฒ”์œ„๋ฅผ ์ค„์ด๊ธฐ ์œ„ํ•ด ์•ž/๋’ค ๋จผ์ € ์ฐพ์„ ๋ฒ”์œ„๋ฅผ ์„ ํƒ
        let startSearch = abs((startTime - time).seconds) < abs((endTime - time).seconds)
        
        var lowerIndex = 0
        var upperIndex = array.count
        
        if startSearch {
            guard let _upperIndex = binarySearchDirection(time: time, isStart: true, range: 0 ..< self.array.count) else {
                return []
            }
            
            upperIndex = _upperIndex
        } else {
            guard let _lowerIndex = binarySearchDirection(time: time, isStart: false, range: 0 ..< self.array.count) else {
                return []
            }
            
            lowerIndex = _lowerIndex
        }
        
        var results: [T] = []
        for index in (lowerIndex..<upperIndex) {
            let item = startSortArray[index]
            if item.range.containsTime(time) {
                results.append(item)
            }
        }
        
        return results
    }
    
    private func binarySearchDirection(time: CMTime, isStart: Bool, range: Range<Int>) -> Int? {
        if range.lowerBound >= range.upperBound {
            let index = range.lowerBound - 1
            if index < 0 {
                return nil
            } else if index < array.count {
                if isStart {
                    return startSortArray[index].range.containsTime(time) ? index + 1 : nil
                } else {
                    return endSortArray[index].range.containsTime(time) ? index : nil
                }
            } else {
                return nil
            }
        }
        
        let midIndex:Int = range.lowerBound + (range.upperBound - range.lowerBound) / 2
        let targetArray = isStart ? startSortArray : endSortArray
        
        let item = targetArray[midIndex]
        let compareValue = isStart ? item.range.start : item.range.end
        
        if compareValue.isNearlyEqualTo(time: time, 0.01.time) {
            return isStart ? midIndex + 1 : midIndex
        } else if compareValue >= time {
            return binarySearchDirection(time: time, isStart: isStart, range: range.lowerBound ..< midIndex)
        } else {
            return binarySearchDirection(time: time, isStart: isStart, range: midIndex + 1 ..< range.upperBound)
        }
    }
}

Test Code

import XCTest
import AVFoundation
@testable import testFramework

class testFrameworkTests: XCTestCase {
    struct TimeData: TimeRange, Equatable {
        var range: CMTimeRange
        
        public static func == (lhs: TimeData, rhs: TimeData) -> Bool {
            return CMTimeRangeEqual(lhs.range, rhs.range)
        }
    }
    
    let timeRangeSearch = FastTimeRangeSearch<TimeData>()
    
    override func setUp() {
        let timeRange0 = TimeData(range: CMTimeRange.init(start: 0.time, end: 2.5.time))
        let timeRange1 = TimeData(range: CMTimeRange.init(start: 2.0.time, end: 10.0.time))
        let timeRange2 = TimeData(range: CMTimeRange.init(start: 3.0.time, end: 5.0.time))
        let timeRange3 = TimeData(range: CMTimeRange.init(start: 4.0.time, end: 5.5.time))
        let timeRange4 = TimeData(range: CMTimeRange.init(start: 4.5.time, end: 7.0.time))
        let timeRange5 = TimeData(range: CMTimeRange.init(start: 7.0.time, end: 7.5.time))
        let timeRange6 = TimeData(range: CMTimeRange.init(start: 7.7.time, end: 8.0.time))
        let timeRange7 = TimeData(range: CMTimeRange.init(start: 9.0.time, end: 15.0.time))
        
        timeRangeSearch.array = [timeRange0, timeRange1, timeRange2, timeRange3,
                                 timeRange4, timeRange5, timeRange6, timeRange7]
    }
    
    func testBinarySearch1() {
        let result1 = timeRangeSearch.list(on: 1.0.time)
        XCTAssertEqual(result1[0], timeRangeSearch.array[0])
    }
    
    func testBinarySearch2() {
        let result3 = timeRangeSearch.list(on: 100.0.time)
        XCTAssertEqual(result3.count, 0)
    }
    
    func testBinarySearch3() {
        let result4 = timeRangeSearch.list(on: 4.5.time)
        let data = timeRangeSearch.array
        XCTAssertEqual(result4, [data[1], data[2], data[3], data[4]])
    }
    
    func testBinarySearch4() {
        let result2 = timeRangeSearch.list(on: 5.7.time)
        let data = timeRangeSearch.array
        XCTAssertEqual(result2, [data[1], data[4]])
    }
    
}

Source Ver.1

import AVFoundation

extension Double {
    var time: CMTime {
        return CMTime(seconds: self, preferredTimescale: 600)
    }
}

extension CMTime {
    public func isNearlyEqualTo(time: CMTime, _ tolerance: CMTime = CMTime(value: 1, timescale: 600)) -> Bool {
        let delta = CMTimeAbsoluteValue(self - time)
        return delta < tolerance
    }
    
}


extension Array {
    func binarySsearch(time: CMTime) -> [Element] {
        guard self.first is CMTimeRange else {
            return []
        }
        
        guard let lowIndex = binarySearchDirection(time: time,
                                                   isStart: true,
                                                   range: 0 ..< self.count) else {
            return []
        }
        
        guard let upperIndex = binarySearchDirection(time: time,
                                                     isStart: false,
                                                     range: 0 ..< self.count) else {
            return []
        }
        
        if lowIndex > upperIndex {
            return []
        }
        
        let range = lowIndex...upperIndex
        print(range)
        
        return self.enumerated().filter({ (item) -> Bool in
            range.contains(item.offset)
        }).map({ (item) -> Element in
            return item.element
        })
    }
    
    func binarySearchDirection(time: CMTime, isStart:Bool, range: Range<Int>) -> Int? {
        guard self is [CMTimeRange] else {
            return nil
        }
        
        if range.lowerBound >= range.upperBound {
            let index = range.lowerBound + (isStart ? 0 : -1)
            if index < self.count, let timeRange = self[index] as? CMTimeRange {
                return timeRange.containsTime(time) ? index : nil
            } else {
                return nil
            }
        }
        
        let midIndex:Int = range.lowerBound + (range.upperBound - range.lowerBound) / 2
        
        guard let timeRange = self[midIndex] as? CMTimeRange else {
            preconditionFailure("impossible")
        }

        let compareValue = isStart ? timeRange.end : timeRange.start
        
        if compareValue.isNearlyEqualTo(time: time, 0.01.time) {
            return midIndex
        } else if compareValue >= time {
            return binarySearchDirection(time: time, isStart: isStart, range: range.lowerBound ..< midIndex)
        } else {
            return binarySearchDirection(time: time, isStart: isStart, range: midIndex + 1 ..< range.upperBound)
        }
        
    }
}

test code

    var data:[CMTimeRange] = []
    override func setUp() {
        // Put setup code here. This method is called before the invocation of each test method in the class.
        let timeRange1 = CMTimeRange(start: .zero, end: 2.5.time)
        let timeRange2 = CMTimeRange(start: 3.0.time, end: 5.0.time)
        let timeRange3 = CMTimeRange(start: 4.0.time, end: 5.5.time)
        let timeRange4 = CMTimeRange(start: 4.5.time, end: 7.0.time)
        let timeRange5 = CMTimeRange(start: 7.0.time, end: 7.5.time)
        let timeRange6 = CMTimeRange(start: 7.7.time, end: 8.0.time)
        let timeRange7 = CMTimeRange(start: 9.0.time, end: 15.0.time)
        
        
        data = [timeRange1, timeRange2, timeRange3,
                                  timeRange4, timeRange5, timeRange6, timeRange7]
        
        data.sort { (lhs, rhs) -> Bool in
            return lhs.start < rhs.start
        }
    }

    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }

    func testBinarySearch1() {
        let result1 = data.binarySsearch(time: 1.0.time)
        XCTAssertEqual(result1[0], data[0])
        
        let result2 = data.binarySsearch(time: 5.7.time)
        XCTAssertEqual(result2.count, 1)
    }
    
    func testBinarySearch2() {
        let result3 = data.binarySsearch(time: 100.0.time)
        XCTAssertEqual(result3.count, 0)
    }
    
    func testBinarySearch3() {
        let result4 = data.binarySsearch(time: 4.5.time)
        XCTAssertEqual(result4, [data[1], data[2], data[3]])
    }
โš ๏ธ **GitHub.com Fallback** โš ๏ธ