Привет всем! Это моя внеочередная статья, о том что нагорело. У меня за последний год накопилось много интересного (и не очень :) ) материала. Но эту статью хочу написать вне очереди. Не так давно я столкнулся с интересным поведением метода seek(to: CMTime). Об этом и хочу написать.

Но начнем по порядку.

AVPlayer известен нам с iOS 4. Он содержит AVPlayerItem, AVAsset.

Основа AVPlayer — AVFoundation, CoreAudio и CoreVideo.

Надеюсь я напишу позднее и про свой триммер, ну а пока на просторах интернета можно найти разные статьи про написание видеоплеера, триммера и тд.

AVPlayer 實踐本地 Cache 功能大全AVPlayer/AVQueuePlayer with AVURLAsset 實作 AVAssetResourceLoaderDelegatemedium.com

How to Trim and Crop Video in Swift | IMG.LY BlogLearn how to use Swift and AVKit to crop a video clip and trim a video timeline with our step-by-step tutorial.img.ly

Но к несчатью качественного материала очень мало, либо тема подзаржавела, либо познавать AVAsset — не легкое дело.

Не так давно я написал триммер видео. Вдохновлением послужил проект Andreas Verhoeven. 

GitHub - AndreasVerhoeven/VideoTrimmerControl: A VideoTrimmer Control for iOSA VideoTrimmer Control for iOS. Contribute to AndreasVerhoeven/VideoTrimmerControl development by creating an account…github.com

В моем триммере — вишенкой на торте стала логика тримма не завязанная на поинтах и пикселях длины редактируемого видео. 

Обычный подход — длина вью равняется 100 % видео. И при перемещении краев мы обрезаем видео. Логика железная — все классно, но задача была поставлена сделать минимальное расстояние между краями обрезка 34 поинта и в совокупности 74 поинта — минимально обрезанное видео, а длительность видео будет варьироваться от 1 сек до 5 сек. Сказано, сделано.

При отладке смотрим, а при движении левого и правого края обрезанного видео, кадры в окне плеера, отображаются с интервалом в 1 сек. — не красиво, надо править.

Здесь возможно следует уточнить, что для обновления кадра в окне плеера, при обрезке видео, я использую метод seek(to: CMTime)

player?.seek(to: time)

Вот тут мы и начинаем смотреть как работает метод AVPlayer —  seek(to: CMTime) и его вариации.

open func seek(to time: CMTime)
open func seek(to time: CMTime, toleranceBefore: CMTime, toleranceAfter: CMTime)

/// group of methods with @available(iOS 5.0, *) 
open func seek(to time: CMTime, completionHandler: @escaping (Bool) -> Void)
open func seek(to time: CMTime) async -> Bool
open func seek(to time: CMTime, toleranceBefore: CMTime, toleranceAfter: CMTime, completionHandler: @escaping (Bool) -> Void)
open func seek(to time: CMTime, toleranceBefore: CMTime, toleranceAfter: CMTime) async -> Bool

Сначала проверяем время CMTime 

let seconds: Double = 103.5867
let time = CMTime(seconds: seconds, preferredTimescale: 1000)

При инициализации CMTime от секунд, также сохраняет милисекунды. Несмотря на то что свойство у CMTime seconds.

let seconds = time.seconds // 103.5867

в консоль CMTime выведется вот так. Тут конечно интересный момент с value и timescale — но его разбирать мы сейчас не будем.

(lldb) po time
▿ CMTime
  - value : 48237
  - timescale : 30000
  ▿ flags : CMTimeFlags
    - rawValue : 1
  - epoch : 0

Таким образом, мы понимаем, что time (CMTime) содержит необходимые нам милисекунды, которые мы передаем в плеер.

player.seek(to: time)

далее смотрим 

await player.seek(to: time)

описание метода феерическое

/**
@method seekToTime:completionHandler:
@abstract Moves the playback cursor and invokes the specified block when the seek operation has either been completed or been interrupted.
@param time
@param completionHandler
@discussion Use this method to seek to a specified time for the current player item and to be notified when the seek operation is complete.
The completion handler for any prior seek request that is still in process will be invoked immediately with the finished parameter
set to NO. If the new request completes without being interrupted by another seek request or by any other operation the specified
completion handler will be invoked with the finished parameter set to YES. If no item is attached, the completion handler will be
invoked immediately with the finished parameter set to NO.
*/

далее

player.seek(to: time) {
...
}

тут с описанием все впорядке. но не работает — плеер обновляет кадр только каждую секунду без промежутков. ?

далее смотрим описание “seekToTime:toleranceBefore:toleranceAfter:”

/**
@method seekToTime:toleranceBefore:toleranceAfter:
@abstract Moves the playback cursor within a specified time bound.
@param time
@param toleranceBefore
@param toleranceAfter
@discussion Use this method to seek to a specified time for the current player item.
The time seeked to will be within the range [time-toleranceBefore, time+toleranceAfter] and may differ from the specified time for efficiency.
Pass kCMTimeZero for both toleranceBefore and toleranceAfter to request sample accurate seeking which may incur additional decoding delay.
Messaging this method with beforeTolerance:kCMTimePositiveInfinity and afterTolerance:kCMTimePositiveInfinity is the same as messaging seekToTime: directly.
*/
open func seek(to time: CMTime, toleranceBefore: CMTime, toleranceAfter: CMTime)

в глаза бросаются слова:

  • and may differ from the specified time for efficiency

  • which may incur additional decoding delay

  • kCMTimePositiveInfinity is the same as messaging seekToTime: directly

после смотрим описание “seekToTime:”

/**
@method seekToTime:
@abstract Moves the playback cursor.
@param time
@discussion Use this method to seek to a specified time for the current player item.
The time seeked to may differ from the specified time for efficiency. For sample accurate seeking see seekToTime:toleranceBefore:toleranceAfter:.
*/

и тут картинка складывается — оказывается плеер ( AVPlayer) — сам решает на какое время обновлять плеер “may differ from the specified time for efficiency” ? и “For sample accurate seeking see seekToTime:toleranceBefore:toleranceAfter:

Таким образом, 

player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)

обновляет плеер по милисекундам. В результате мы ставим бан а использование “seekToTime” и используем только “seekToTime:toleranceBefore:toleranceAfter:”.

Выводы:

использование “seekToTime” в плеере приведет к неожиданному результату,

почему “seekToTime” решил обновлять видео посекундно известно только Apple,

далее используем только “seekToTime:toleranceBefore:toleranceAfter:”. ?

Комментарии (2)


  1. anticyclope
    22.05.2024 03:19

    Мне кажется, любой, кто мало-мальски понимает в том, как устроено сжатое видео придёт в комменты и безотносительно опыта в iOS и AVPlayer накидает автору в панамку про I, B и P фреймы и про то, что "неожиданные результаты" вполне себе ожиданные.

    Впрочем ничего нового.


    1. tvrrp
      22.05.2024 03:19

      Это круто и прикольно, но документация эпла оставляет желать лучшего. И это только верхушка всех странных поведений в AVFoundation.