Найти в Дзене
АйТи Новостник

Как настроить видеоистории в стиле Instagram в вашем приложении

В статье вы узнаете, как показывать несколько видео в одном окне, как мы видим в Instagram Stories.
Мы также узнаем, как кэшировать видео на устройстве пользователя, чтобы помочь сохранить данные и сетевые вызовы этого пользователя и упростить их работу.
Небольшое примечание: эта реализация предназначена для iOS, но та же логика может применяться и в других базах кода.
Как правило, всякий раз,
Оглавление

В статье вы узнаете, как показывать несколько видео в одном окне, как мы видим в Instagram Stories.

Мы также узнаем, как кэшировать видео на устройстве пользователя, чтобы помочь сохранить данные и сетевые вызовы этого пользователя и упростить их работу.

Небольшое примечание: эта реализация предназначена для iOS, но та же логика может применяться и в других базах кода.

-2

Как правило, всякий раз, когда мы хотим воспроизвести видео, мы получаем URL-адрес видео и просто представляем его AVPlayerViewController.

let videoURL = URL(string: "Sample-Video-Url")
let player = AVPlayer(url: videoURL!)
let playerViewController = AVPlayerViewController()
playerViewController.player = player
self.present(playerViewController, animated: true) {
playerViewController.player.play()
}

AVPlayerViewController.swift

Довольно просто, правда?

Но недостатком этой реализации является то, что ее нельзя настроить . Если вы работаете в хорошей производственной компании, то это будет повседневный вопрос. : D

В качестве альтернативы мы можем использовать AVPlayerLayerwhich, который будет выполнять аналогичную работу, но он позволяет нам настраивать представление и другие элементы.

let videoURL = URL(string: "Sample-Video-Url")
let player = AVPlayer(url: videoURL!)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.view.bounds
self.view.layer.addSublayer(playerLayer)
player.play()

AVPlayerLayer.swift

Но что, если вы хотите объединить несколько видеороликов, похожих на истории из Instagram ? Тогда нам, вероятно, придется погрузиться немного глубже.

Возвращаясь к формулировке проблемы

Теперь позвольте мне рассказать вам о моем случае использования.

В моей компании Swiggy мы хотим иметь возможность показывать несколько видеороликов, при этом каждое видео должно отображаться x количество раз.

Вдобавок ко всему, в нем должна быть функция историй в стиле Instagram.

  • Видео-2 должно автоматически воспроизводиться после видео-1 и т. Д.
  • Он должен переходить к соответствующим видео всякий раз, когда пользователь нажимает влево или вправо.

Если вы думаете, что кеширование может быть ответом, не волнуйтесь - я вернусь к этому чуть позже.

Несколько слоев на одном экране

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

Что мы можем сделать, так это создать его AVPlayerLayerи назначить ему первое видео. Когда первое видео закончено, мы назначаем ему следующее видео AVPlayerLayer.

func addPlayer(player: AVPlayer) {
player.currentItem?.seek(to: CMTime.zero, completionHandler: nil)
playerViewModel?.player = player
playerView.playerLayer.player = player
}

Change_Video.swift

Чтобы перейти к предыдущему или следующему видео, мы можем сделать следующее:

  • Добавьте жест касания на вид
  • Если место касания 'x' меньше половины экрана, тогда назначьте предыдущее видео, иначе назначьте следующее видео
@objc func didTapSnap(_ sender: UITapGestureRecognizer) {
let touchLocation = sender.location(ofTouch: 0, in: view)
if touchLocation.x < view.frame.width/2 {
changePlayer(forward: false)
}
else {
fillupLastPlayedSnap()
changePlayer(forward: true)
}
}

Handle_Tap.swift

Вот и все. Теперь у нас есть собственная функция видео в стиле Insta Stories.

Но наша задача еще не выполнена!

Теперь вернемся к кешированию

Мы не хотим, чтобы каждый раз, когда пользователь переходит от одного видео к другому, он начинает загружать видео с самого начала.

Кроме того, если видео снова отображается в следующем сеансе, нам не нужно выполнять еще один вызов сервера.

Если мы сможем кэшировать видео, интернет пользователя будет сохранен. Также снизится нагрузка на сервер.

Наконец, UX улучшится, так как пользователю не придется долго ждать загрузки видео.

Как хороший разработчик, сокращение использования Интернета пользователями должно быть нашим приоритетом.

Загружать видео асинхронно

Первое, что мы можем использовать для загрузки видео, - это loadValuesAsynchronously .

Согласно документации Apple , loadValuesAsynchronously:

Указывает активу загрузить значения всех указанных ключей (имен свойств), которые еще не загружены.

Преимущество здесь в том, что он сохраняет видео до его рендеринга. Таким образом, он не будет загружать видео с самого начала, когда пользователь перейдет к предыдущему видео. Он загрузит только ту часть, которая ранее не рендерилась.

Давайте посмотрим на пример : допустим, у нас есть Video_1 длительностью 15 секунд, и пользователь просмотрел 10 секунд этого видео, прежде чем перейти к Video_2.

Теперь, если пользователь снова вернется к Video_1, нажав влево, loadValuesAsynchronously сохранит эти 10 секунд видео и загрузит только оставшиеся (без наблюдения) 5 секунд.

func asynchronouslyLoadURLAssets(_ newAsset: AVURLAsset) {
DispatchQueue.main.async {
newAsset.loadValuesAsynchronously(forKeys: self.assetKeysRequiredToPlay) {
for key in self.assetKeysRequiredToPlay {
var error: NSError?
if newAsset.statusOfValue(forKey: key, error: &error) == .failed {
self.delegate?.playerDidFailToPlay(message: "Can't use this AVAsset because one of it's keys failed to load")
return
}
}

if !newAsset.isPlayable || newAsset.hasProtectedContent {
self.delegate?.playerDidFailToPlay(message: "Can't use this AVAsset because it isn't playable or has protected content")
return
}
let currentItem = AVPlayerItem(asset: newAsset)
let currentPlayer = AVPlayer(playerItem: currentItem)
self.delegate?.playerDidSuccesToPlay(playerDetail: currentPlayer)
}

}

loadValuesAsynchronously.swift

Вы можете найти более подробную информацию о loadValuesAsynchronously по этой ссылке .

Предостережение здесь в том, что видеоданные сохраняются только для этого сеанса. Если пользователь закрывается и возвращается в приложение, видео необходимо снова загрузить.

Итак, какие еще варианты у нас есть?

Сохранение видео на устройстве

Теперь идет кэширование видео !

Когда видео полностью отрисовано, мы можем экспортировать видео и сохранить его на устройстве пользователя. Когда видео снова появится в их следующем сеансе, мы можем выбрать видео с устройства и просто загрузить его.

AVAssetExportSession
Согласно
документации Apple :

Объект, который перекодирует содержимое исходного объекта актива для создания выходных данных в форме, описанной заданной предустановкой экспорта.

Это означает, что AVAssetExportSession действует как экспортер, с помощью которого мы можем сохранить файл на устройство пользователя. Мы должны указать выходной URL и тип выходного файла.

let exporter = AVAssetExportSession(asset: avUrlAsset, presetName: AVAssetExportPresetHighestQuality)
exporter?.outputURL = outputURL
exporter?.outputFileType = AVFileType.mp4

exporter?.exportAsynchronously(completionHandler: {
print(exporter?.status.rawValue)
print(exporter?.error)
})

AVAssetExportSession.swift

Вы можете найти более подробную информацию об AVAssetExportSession по этой ссылке .

Теперь осталось только извлечь данные из кеша и загрузить видео.

Перед загрузкой проверьте, есть ли видео в кеше. Затем получите этот локальный URL-адрес и передайте его loadValuesAsynchronously.

if let cacheUrl = FindCachedVideoURL(forVideoId: videoId) {
let cacheAsset = AVURLAsset(url: cacheUrl)
asynchronouslyLoadURLAssets(cacheAsset)
}
else {
asynchronouslyLoadURLAssets(newAsset)
}

Fetch_Local_Video.swift

Кэширование поможет снизить объем использования пользовательских данных, а также нагрузку на сервер (иногда до ТБ данных).

Другие варианты использования кеширования

Какие еще варианты использования мы можем обрабатывать с помощью кеширования? Ниже приведены примеры использования кэширования:

Обеспечьте оптимальное хранение

Перед сохранением видео на устройстве вы должны проверить, достаточно ли на устройстве памяти для этого.

func isStorageAvailable() -> Bool {
let fileURL = URL(fileURLWithPath: NSHomeDirectory() as String)
do {
let values = try fileURL.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey, .volumeTotalCapacityKey])
guard let totalSpace = values.volumeTotalCapacity,
let freeSpace = values.volumeAvailableCapacityForImportantUsage else {
return false
}
if freeSpace > minimumSpaceRequired {
return true
} else {
// Capacity is unavailable
return false
}
catch {}
return false
}

Storage_Check.swift

Удалить устаревшие видео

У вас может быть временная метка для каждого видео, чтобы вы могли удалить старые видео из памяти устройства через определенное количество дней.

func cleanExpiredVideos() {
let currentTimeStamp = Date().timeIntervalSince1970
var expiredKeys: [String] = []
for videoData in videosDict where currentTimeStamp - videoData.value.timeStamp >= expiryTime {
// video is expired. delete
if let _ = popupVideosDict[videoData.key] {
expiredKeys.append(videoData.key)
}
}
for key in expiredKeys {
if let _ = popupVideosDict[key] {
popupVideosDict.removeValue(forKey: key)
deleteVideo(ForVideoId: key)
}
}
}

TimeStamp_Check.swift

Сохраняйте ограниченное количество видео

Вы можете убедиться, что в файле одновременно сохраняется только ограниченное количество видео. Допустим, 10.

Затем, когда появится 11-е видео, вы можете удалить наименее просматриваемое видео и заменить его новым. Это также поможет вам не потреблять слишком много памяти устройства пользователя.

func removeVideoIfMaxNumberOfVideosReached() {
if popupVideosDict.count >= maxVideosAllowed {
// remove the least recently used video
let sortedDict = popupVideosDict.keysSortedByValue { (v1, v2) -> Bool in
v1.timeStamp < v2.timeStamp
}
guard let videoId = sortedDict.first else {
return
}
popupVideosDict.removeValue(forKey: videoId)
deleteVideo(ForVideoId: videoId)
}
}

MaxNumberOfVideos.swift

Измерение воздействия

Не забудьте добавить журналы, чтобы вы могли измерить влияние вашей функции. Для этого я использовал настраиваемое событие журнала New Relic:

static func findCachedVideoURL(forVideoId id: String) -> URL? {
let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
if let dirPath = paths.first {
let fileURL = URL(fileURLWithPath: dirPath).appendingPathComponent(folderPath).appendingPathComponent(id + ".mp4")
let filePath = fileURL.path
let fileManager = FileManager.default
if fileManager.fileExists(atPath: filePath) {
NewRelicService.sendCustomEvent(with: NewRelicEventType.statusCodes,
eventName: NewRelicEventName.videoCacheHit,
attributes: [NewRelicAttributeKey.videoSize: fileURL.fileSizeString])
return fileURL
} else {
return nil
}
}
return nil
}

Logging.swift

Чтобы преобразовать размер файла в удобочитаемый формат, я получаю размер файла и конвертирую его в мегабайты.

extension URL {
var attributes: [FileAttributeKey : Any]? {
do {
return try FileManager.default.attributesOfItem(atPath: path)
} catch let error as NSError {
print("FileAttribute error: \(error)")
}
return nil
}

var fileSize: UInt64 {
return attributes?[.size] as? UInt64 ?? UInt64(0)
}

var fileSizeString: String {
return ByteCountFormatter.string(fromByteCount: Int64(fileSize), countStyle: .file)
}
}

File_Convert.swift

Вот как вы можете измерить свое влияние:

-4

Total data saved = number of requests * video_size = 2.4MB*20.3K ~= 49GB

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

Это оно! Вы создали свой собственный механизм кэширования.

-5