Uma nota rápida: esta implementação é para iOS, mas a mesma lógica pode ser aplicada em outras bases de código também.

ezgif.com video to gif 5

Em geral, sempre que queremos reproduzir um vídeo, obtemos o URL do vídeo e simplesmente apresentamos AVPlayerViewController com esse URL.

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

Muito simples, certo?

Mas a desvantagem dessa implementação é que você não pode personalizare isso. O que, se você estiver trabalhando para uma boa empresa de produtos, será uma pergunta cotidiana. : D

Alternativamente, podemos usar AVPlayerLayer que fará um trabalho semelhante – mas nos permite personalizar a visualização e outros elementos.

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

Mas e se você quiser combinar vários vídeos, semelhantes a Histórias do Instagram? Então provavelmente teremos que mergulhar um pouco mais fundo.

Voltando à Declaração do Problema

Agora, deixe-me falar sobre meu caso de uso.

Na minha empresa, Swiggy, queremos poder mostrar vários vídeos, onde cada vídeo deve ser mostrado x número de vezes.

Além disso, deve ter um recurso de histórias como o Instagram.

  • Video-2 deve ser reproduzido automaticamente após o video-1 e assim por diante
  • Deve pular para os vídeos correspondentes sempre que o usuário tocar para a esquerda ou direita.

Se você acha que o cache pode ser a resposta, não se preocupe – irei abordar isso em breve.

Várias camadas em uma vista

Em primeiro lugar, precisamos descobrir como adicionar vários vídeos em uma visualização.

O que podemos fazer é criar um AVPlayerLayer e atribuir o primeiro vídeo a ele. Quando o primeiro vídeo termina, atribuímos o próximo vídeo ao mesmo AVPlayerLayer .

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

Para pular para o vídeo anterior ou seguinte, podemos fazer o seguinte:

  • Adicionar um gesto de toque na vista
  • Se o local de toque ‘x’ for menos da metade da tela, atribua o vídeo anterior, caso contrário, atribua o próximo vídeo

@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

Aqui vamos nós. Agora temos nosso próprio recurso de vídeo Insta-like Stories.

Mas nossa tarefa ainda não acabou!

Agora, de volta ao cache

Não queremos que sempre que um usuário navegar de um vídeo para outro, ele comece a baixar o vídeo desde o início.

Além disso, se o vídeo for mostrado novamente na próxima sessão, não precisamos fazer outra chamada de servidor.

Se pudermos armazenar o vídeo em cache, a Internet do usuário será salva. A carga no servidor também será reduzida.

Finalmente, a UX vai melhorar, pois o usuário não terá que esperar muito para carregar o vídeo.

Como um bom desenvolvedor, reduzindo uma o uso da Internet pelo usuário deve ser nossa prioridade.

Carregar Vídeos De forma assíncrona

A primeira coisa que podemos usar para carregar vídeos é loadValuesAsynchronously.

De acordo com a documentação da Apple, loadValuesAsynchronously:

Diz ao ativo para carregar os valores de todas as chaves especificadas (nomes de propriedade) que ainda não foram carregados.

A vantagem aqui é que ele salva o vídeo até que seja renderizado. Portanto, ele não baixará o vídeo desde o início sempre que o usuário navegar para um vídeo anterior. Ele apenas baixará a parte que não foi renderizada anteriormente.

Vejamos um example: digamos que temos Video_1 com 15 segundos de duração e o usuário viu 10 segundos desse vídeo antes de pular para Video_2.

Agora, se o usuário voltar ao Video_1 novamente tocando à esquerda, loadValuesAsynchronously terá aqueles 10 segundos de vídeo salvos e baixará apenas os 5 segundos restantes (não assistidos).

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

Você pode encontrar mais detalhes em loadValuesAsynchronously em isto ligação.

A ressalva aqui é que os dados de vídeo persistem apenas para aquela sessão. Se o usuário fechar e voltar ao aplicativo, o vídeo deverá ser baixado novamente.

Então, que outras opções temos?

Salvando vídeos no dispositivo

Agora vem Cache de vídeo!

Quando o vídeo é renderizado completamente, podemos exportar o vídeo e salvá-lo no dispositivo do usuário. Quando o vídeo voltar na próxima sessão, podemos pegar o vídeo do dispositivo e simplesmente carregá-lo.

AVAssetExportSession
De acordo com Documentação da Apple:

Um objeto que transcodifica o conteúdo de um objeto de origem de ativo para criar uma saída da forma descrita por uma predefinição de exportação especificada.

Isso significa que AVAssetExportSession atua como um exportador, por meio do qual podemos salvar o arquivo no dispositivo do usuário. Temos que fornecer a URL de saída e o tipo de arquivo de saída.

let exporter = AVAssetExportSession(asset: avUrlAsset, presetName: AVAssetExportPresetHighestQuality)exporter?.outputURL = outputURLexporter?.outputFileType = AVFileType.mp4exporter?.exportAsynchronously(completionHandler: {	print(exporter?.status.rawValue)	print(exporter?.error)})
AVAssetExportSession.swift

Você pode encontrar mais detalhes em AVAssetExportSession neste ligação.

Agora, a única coisa que resta é buscar os dados do cache e carregar o vídeo.

Antes de carregar, verifique se o vídeo está presente no cache. Em seguida, busque esse URL local e dê-o a loadValuesAsynchronously.

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

O armazenamento em cache ajudará a reduzir muito o uso de dados do usuário, bem como a carga do servidor (às vezes até TBs de dados).

Outros casos de uso para cache

Que outros casos de uso podemos tratar com o cache? A seguir estão exemplos de maneiras como você pode usar o cache aqui:

Garanta armazenamento ideal

Antes de salvar o vídeo no dispositivo, você deve verificar se há armazenamento suficiente no dispositivo para fazer isso.

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

Remover vídeos obsoletos

Você pode ter um carimbo de data / hora para cada vídeo, de modo que possa limpar vídeos antigos da memória do dispositivo após um determinado número de dias.

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

Manter um número limitado de vídeos

Você pode garantir que apenas um número limitado de vídeos sejam salvos no arquivo por vez. Digamos 10.

Então, quando o 11º vídeo chegar, você pode fazer com que ele exclua o vídeo menos assistido e substitua-o pelo novo. Isso também ajudará você a não consumir muito da memória do dispositivo do usuário.

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

Medir o impacto

Não se esqueça de adicionar logs, para que você possa medir o impacto de seu recurso. Usei um evento de registro personalizado da New Relic para fazer isso:

 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

Para converter o tamanho do arquivo em um formato legível, eu busco o tamanho do arquivo e o converto em Mbs.

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

É assim que você pode medir seu impacto:

Screenshot 2020 09 16 at 11.34.24 AM

Total de dados salvos = número de pedidos * video_size = 2,4 MB * 20,3 K ~ = 49 GB

São apenas duas semanas de dados. Você faz a matemática para o ano inteiro. 😁 E isso vai continuar aumentando exponencialmente com o tempo.

É isso aí! Agora você construiu seu próprio mecanismo de cache.

yay

Neste artigo, vimos como podemos facilmente integrar vários vídeos em uma visualização, fornecendo um recurso de história semelhante ao do Instagram.

Também aprendemos por que e como o cache desempenha um papel importante aqui. Vimos como ele ajuda o usuário a economizar muitos dados e a ter uma experiência de usuário tranquila.

Avise-me se perdi algo ou se você puder pensar em mais alguns casos de uso.
Obrigado pelo seu tempo. 🙂