Publié par

Il y a 6 mois -

Temps de lecture 6 minutes

ReplayKit

En général, pour signaler un bug ou comportement bizarre, l’utilisateur doit remplir un formulaire et expliquer le problème. Ceci n’est pas toujours facile à faire avec succès. Donc une meilleure solution serait d’enregistrer dans le bug une video.

Je vous propose de se servir de ReplayKit, une API introduite avec iOS 9, pour proposer aux utilisateurs d’enregistrer les possibles anomalies de votre application.

ReplayKit vous permet d’enregistrer l’écran de votre application avec la permission de l’utilisateur et sans autre action de sa part et ceci pourrait suffire pour certains. Si on veut aller plus loin ReplayKit nous permet, à partir de iOS 11, d’enregistrer l’écran de notre portable même à l’extérieur de notre application. C’est cette dernière fonctionnalité que l’on va utiliser.

Notre extension est indépendante de notre application principale mais si on crée un App Group, cela nous permet de partager un répertoire et des UserDefaults entre les 2 processus.

Premièrement, il faut ajouter l’extension. Pour cela on ajoute un nouveau Target et on cherche l’extension «Broadcast Upload Extension»

~ Choisir l’extension Broadcast Upload Extension ~

~ Décocher l’UI extension ~

Après tout cela notre extension peut être visible dans notre Centre de Contrôle, pour cela il faut ajouter «Enregistrement de l’écran» dans le Centre de Contrôle :

Ensuite il faut «3D-touch» ou appuyer longtemps sur le bouton «Enregistrement de l’écran» pour pouvoir voir notre extension :

Maintenant que nous avons tout configuré, passons au code généré par Xcode de notre extension:

Code généré par Xcode
import ReplayKit

class SampleHandler: RPBroadcastSampleHandler {

    override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) {
        // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional. 
    }
    
    override func broadcastPaused() {
        // User has requested to pause the broadcast. Samples will stop being delivered.
    }
    
    override func broadcastResumed() {
        // User has requested to resume the broadcast. Samples delivery will resume.
    }
    
    override func broadcastFinished() {
        // User has requested to finish the broadcast.
    }
    
    override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
        switch sampleBufferType {
            case RPSampleBufferType.video:
                // Handle video sample buffer
                break
            case RPSampleBufferType.audioApp:
                // Handle audio sample buffer for app audio
                break
            case RPSampleBufferType.audioMic:
                // Handle audio sample buffer for mic audio
                break
        }
    }
}

On se servira des fonctions: broadcastStarted, broadcastFinished et processSampleBuffer.
Maintenant on peut créer notre App Group, dans notre compte développeur Apple. Notre AppGroupID sera: «group.replaykitexample.xebia»

et puis l’ajouter l’App Group qu’on vient de créer à notre App ID

 

Ensuite on pourra ajouter l’App group dans les Capabilities de notre application sur Xcode

~ Ajouter un App Group dans Capabilities de 2 targets ~

~ Cocher l’App Group ~

Après avoir mis tout cela en place, on peut s’attaquer au code.
Pour traiter le flux video et audio et pouvoir créer un fichier video à la fin, on utilisera AVFoundation

class SampleHandler: RPBroadcastSampleHandler {    
    var assetWriter: AVAssetWriter!
    var assetWriterInputVideo: AVAssetWriterInput!
    var assetWriterInputAudio: AVAssetWriterInput!
    
	var isFirstSample: Bool = true
	let appGroupId = "group.replaykitexample.xebia"

	...

Ce sont ces 3 objets qui nous permettront de créer le fichier final.

Un CMSampleBuffer est un object qui contient des échantillons compressés d’un type particulier de media (ex: video, audio, subtitles, etc …)

Un AVAssetWriter est un objet utilisé pour écrire des données multimédia en forme de CMSampleBuffer dans un fichier (ex: mp4)

Un AVAssetWriterInput est un object qui ajoute les ‘samples’ (CMSampleBuffer) à une seule piste (ex: video, audio, subtitles, etc …) du fichier de sortie du ‘assetWriter’. Dans notre cas, nous allons avoir 2, un pour la video et un autre pour l’audio.

Avec notre group ID on accède à notre container partagé ensuite on initialise nos objets. On utilise AVOutputSettingsAssistant pour obtenir des paramètres pour notre fichier de sortie (paramètres par défaut)

    override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) {

        let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupID)!
        let folder = containerURL.appendingPathComponent("folder", isDirectory: true)
        try! FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true, attributes: nil)
        
        let newFileURL = folder.appendingPathComponent("newVideo.mp4")
        
        let preset = AVOutputSettingsAssistant(preset: .preset1280x720)

        assetWriter = try! AVAssetWriter(outputURL: newFileURL, fileType: .mp4)
        assetWriterInputVideo = AVAssetWriterInput(mediaType: .video, outputSettings: preset?.videoSettings)
        assetWriterInputAudio = AVAssetWriterInput(mediaType: .audio, outputSettings: preset?.audioSettings)
        assetWriterInputVideo.expectsMediaDataInRealTime = true
        assetWriterInputAudio.expectsMediaDataInRealTime = true
        
        assetWriter.add(assetWriterInputVideo)
        assetWriter.add(assetWriterInputAudio)

        assetWriter.startWriting()
    }

Pour que la video soit bien synchronisée, il faut déterminer le timestamp du premier sampleBuffer. Ensuite on peut ajouter le ‘sample’ s’il est prêt, valide et notre assetWriterInputAutio est disponible.

    override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
        
        if isFirstSample {
            let timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
            assetWriter.startSession(atSourceTime: timestamp)
            isFirstSample = false;
        }

        switch sampleBufferType {
        case RPSampleBufferType.video:
            if CMSampleBufferIsValid(sampleBuffer) && CMSampleBufferDataIsReady(sampleBuffer), assetWriterInputVideo.isReadyForMoreMediaData {
                assetWriterInputVideo.append(sampleBuffer)
            }
        case RPSampleBufferType.audioApp:
            break
        case RPSampleBufferType.audioMic:
            if CMSampleBufferIsValid(sampleBuffer) && CMSampleBufferDataIsReady(sampleBuffer), assetWriterInputAudio.isReadyForMoreMediaData {
                assetWriterInputAudio.append(sampleBuffer)
            }
        }
    }

Le processus de l’extension se termine une fois la fonction broadcastFinished a fini de s’exécuter même si l’enregistrement de la video n’a pas fini correctement. Ceci peut corrompre la video et la rendre inutilisable.
Pour résoudre cette problématique, on peut utiliser un dispatchGroup pour attendre que le fichier soit prêt avant que le processus soit terminé.

    override func broadcastFinished() {

        let dispatchGroup = DispatchGroup()

        self.assetWriterInputVideo.markAsFinished()
        self.assetWriterInputAudio.markAsFinished()
    
        dispatchGroup.enter()
        self.assetWriter.finishWriting {
            dispatchGroup.leave()
        }
        dispatchGroup.wait()
    }

Après la video créée, on pourra accéder à la video à partir de notre application principale en utilisant le group app id comme on l’a fait quand on a créer l’URL au début.

    var newFileURL: URL?
    var groupID = "group.replaykitexample.xebia"

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupID)!
        let folder = containerURL.appendingPathComponent("folder", isDirectory: true)
        newFileURL = folder.appendingPathComponent("newVideo.mp4")

	}

Conclusion :

Bien que cette solution nous permette d’enregistrer l’écran en dehors de l’application, elle vient avec quelques inconvénients comme par exemple le fait de ne pas pouvoir la désactiver. L’extension sera toujours une option pour l’utilisateur.
Un autre inconvénient est le fait de ne pas pouvoir gérer d’une manière satisfaisante l’enregistrement de la video, vu qu’il se passe dans un autre processus, comme par exemple, faire une pause, ce type d’enregistrement n’est pas possible.

Pour aller plus loin :

Une fois la video disponible sur l’application principale on peut penser par exemple :

  • à l’envoyer sur un serveur (AWS, GCS)
  • à la sauvegarder dans les photos de l’utilisateur s’il le souhaite ou la partager.
  • à l’éditer

Publié par

Publié par Patricio Guzman

Développeur iOS chez Xebia. Il aime le code propre et testable.

Commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.