Audio

AAudio è una nuova API di Android C introdotta nella release di Android O. È progettata per applicazioni audio ad alte prestazioni che richiedono una bassa latenza. Le app comunicano con AAudio leggendo e scrivendo dati negli stream.

La progettazione dell'API AAudio è minima, non svolge le seguenti funzioni:

  • Enumerazione dei dispositivi audio
  • Routing automatico tra endpoint audio
  • I/O file
  • Decodifica di audio compresso
  • Presentazione automatica di tutti gli input/flussi di dati in un unico callback.

Per iniziare

Puoi chiamare AAudio dal codice C++. Per aggiungere il set di funzionalità AAudio alla tua app, includi il file di intestazione AAudio.h:

#include <aaudio/AAudio.h>

Stream audio

L'audio sposta i dati audio tra la tua app e gli ingressi e le uscite audio sul tuo dispositivo Android. L'app trasmette i dati in entrata e in uscita leggendo e scrivendo negli stream audio, rappresentati dalla struttura AAudioStream. Le chiamate di lettura/scrittura possono essere bloccanti o non bloccanti.

Uno stream viene definito da quanto segue:

  • Il dispositivo audio che rappresenta l'origine o il sink dei dati nello stream.
  • La modalità di condivisione che determina se uno stream ha accesso esclusivo a un dispositivo audio che altrimenti potrebbe essere condiviso tra più stream.
  • Il formato dei dati audio nello stream.

Dispositivo audio

Ogni stream è collegato a un singolo dispositivo audio.

Un dispositivo audio è un'interfaccia hardware o un endpoint virtuale che funge da sorgente o sink per un flusso continuo di dati audio digitali. Non confondere un dispositivo audio (microfono o cuffie Bluetooth integrati) con il dispositivo Android (smartphone o orologio) su cui è in esecuzione la tua app.

Puoi utilizzare il metodo AudioManagergetDevices() per trovare i dispositivi audio disponibili sul tuo dispositivo Android. Il metodo restituisce informazioni su type di ogni dispositivo.

Sul dispositivo Android, ogni dispositivo audio ha un ID univoco. Puoi utilizzare l'ID per associare uno stream audio a un dispositivo audio specifico. Tuttavia, nella maggior parte dei casi puoi consentire ad AAudio di scegliere il dispositivo principale predefinito anziché di specificarne uno manualmente.

Il dispositivo audio collegato a uno stream determina se lo stream viene utilizzato per l'input o l'output. Un flusso può spostare i dati solo in una direzione. Quando definisci uno stream, ne imposti anche la direzione. Quando apri uno stream, Android controlla che il dispositivo audio e la direzione dello streaming coincidano.

Modalità di condivisione

Uno stream ha una modalità di condivisione:

  • AAUDIO_SHARING_MODE_EXCLUSIVE indica che lo stream ha accesso esclusivo al relativo dispositivo audio. il dispositivo non può essere usato da altri stream audio. Se il dispositivo audio è già in uso, lo stream potrebbe non avere l'accesso esclusivo. Gli stream esclusivi hanno probabilmente una latenza più bassa, ma hanno anche maggiori probabilità di disconnettersi. Dovresti chiudere gli stream esclusivi non appena non ti servono più, in modo che altre app possano accedere al dispositivo. Gli stream esclusivi offrono la latenza più bassa possibile.
  • AAUDIO_SHARING_MODE_SHARED consente ad AAudio di mixare l'audio. AAudio mixa tutti gli stream condivisi assegnati allo stesso dispositivo.

Puoi impostare esplicitamente la modalità di condivisione quando crei uno stream. Per impostazione predefinita, la modalità di condivisione è SHARED.

Formato audio

I dati trasmessi attraverso uno stream hanno i consueti attributi audio digitali. Le riportiamo di seguito:

  • Formato dei dati di esempio
  • Numero di canali (campioni per frame)
  • Frequenza di campionamento

AAudio consente i seguenti formati di esempio:

formato_audio_t Tipo di dati C Note
AAUDIO_FORMAT_PCM_I16 int16_t campioni comuni a 16 bit, formato Q0.15
AAUDIO_FORMAT_PCM_FLOAT in virgola mobile Da -1,0 a +1,0
AAUDIO_FORMAT_PCM_I24_PACKED uint8_t in gruppi di 3 campioni a 24 bit compressi, formato Q0.23
AAUDIO_FORMAT_PCM_I32 int32_t campioni comuni a 32 bit, formato Q0.31
AAUDIO_FORMAT_IEC61937 uint8_t audio compresso con wrapping in IEC61937 per passthrough HDMI o S/PDIF

Se richiedi un formato di esempio specifico, lo stream utilizzerà quel formato. anche se il formato non è ottimale per il dispositivo. Se non specifichi un formato di esempio, AAudio sceglierà quello ottimale. Dopo l'apertura del flusso, devi eseguire una query sul formato dei dati di esempio converti i dati se necessario, come in questo esempio:

aaudio_format_t dataFormat = AAudioStream_getDataFormat(stream);
//... later
if (dataFormat == AAUDIO_FORMAT_PCM_I16) {
     convertFloatToPcm16(...)
}

Creazione di uno stream audio

La libreria AAudio segue un pattern di progettazione del builder e fornisce AAudioStreamBuilder.

  1. Crea un AAudioStreamBuilder:

    AAudioStreamBuilder *builder;
    aaudio_result_t result = AAudio_createStreamBuilder(&builder);
    

  2. Imposta la configurazione dello stream audio nello strumento di creazione, utilizzando le funzioni di creazione che corrispondono ai parametri dello stream. Sono disponibili le seguenti funzioni set facoltative:

    AAudioStreamBuilder_setDeviceId(builder, deviceId);
    AAudioStreamBuilder_setDirection(builder, direction);
    AAudioStreamBuilder_setSharingMode(builder, mode);
    AAudioStreamBuilder_setSampleRate(builder, sampleRate);
    AAudioStreamBuilder_setChannelCount(builder, channelCount);
    AAudioStreamBuilder_setFormat(builder, format);
    AAudioStreamBuilder_setBufferCapacityInFrames(builder, frames);
    

    Tieni presente che questi metodi non segnalano errori, come una costante non definita o un valore fuori intervallo.

    Se non specifichi il valore deviceId, il valore predefinito è il dispositivo di output principale. Se non specifichi la direzione del flusso, il valore predefinito è un flusso di output. Per tutti gli altri parametri, puoi impostare esplicitamente un valore oppure lasciare che il sistema assegnare il valore ottimale non specificando il parametro o impostando a AAUDIO_UNSPECIFIED.

    Per sicurezza, controlla lo stato dello stream audio dopo averlo creato, come spiegato di seguito nel passaggio 4.

  3. Una volta configurato AAudioStreamBuilder, utilizzalo per creare uno stream:

    AAudioStream *stream;
    result = AAudioStreamBuilder_openStream(builder, &stream);
    

  4. Dopo aver creato il flusso, verificane la configurazione. Se hai specificato formato del campione, frequenza di campionamento o campioni per frame non cambieranno. Se hai specificato la modalità di condivisione o la capacità di buffer, potrebbero cambiare a seconda delle capacità del dispositivo audio dello stream e Il dispositivo Android su cui è in esecuzione. Una questione di buona capacità difensiva programmazione, devi controllare la configurazione dello stream prima di utilizzarlo. Esistono funzioni per recuperare l'impostazione di streaming che corrisponde impostazione builder:

    AAudioStreamBuilder_setDeviceId() AAudioStream_getDeviceId()
    AAudioStreamBuilder_setDirection() AAudioStream_getDirection()
    AAudioStreamBuilder_setSharingMode() AAudioStream_getSharingMode()
    AAudioStreamBuilder_setSampleRate() AAudioStream_getSampleRate()
    AAudioStreamBuilder_setChannelCount() AAudioStream_getChannelCount()
    AAudioStreamBuilder_setFormat() AAudioStream_getFormat()
    AAudioStreamBuilder_setBufferCapacityInFrames() AAudioStream_getBufferCapacityInFrames()

  5. Puoi salvare lo strumento per la creazione e riutilizzarlo in futuro per creare altri stream. Se invece non prevedi di usarla più, dovresti eliminarla.

    AAudioStreamBuilder_delete(builder);
    

Utilizzo di uno stream audio

Transizioni di stato

In genere, uno stream AAudio si trova in uno dei cinque stati stabili (lo stato di errore, Scollegato, è descritto alla fine di questa sezione):

  • Apri
  • Avviato
  • In pausa
  • Faccina rossa in viso con occhi spalancati
  • Interrotto

I dati passano attraverso un flusso solo quando è nello stato Iniziato. A spostare un flusso da uno stato all'altro, utilizzare una delle funzioni che richiedono uno stato transizione:

aaudio_result_t result;
result = AAudioStream_requestStart(stream);
result = AAudioStream_requestStop(stream);
result = AAudioStream_requestPause(stream);
result = AAudioStream_requestFlush(stream);

Tieni presente che puoi richiedere la messa in pausa o lo svuotamento solo su un flusso di output:

Queste funzioni sono asincrone e la modifica dello stato non avviene immediatamente. Quando richiedi una modifica di stato, il flusso sposta uno dei stati temporanei corrispondenti:

  • Iniziale
  • Sospensione in corso...
  • Lavaggio
  • Interruzione in corso
  • Chiusura

Il diagramma degli stati seguente mostra gli stati stabili come rettangoli arrotondati e gli stati transitori come rettangoli punteggiati. Anche se non viene mostrato, puoi chiamare close() da qualsiasi stato

Ciclo di vita AAudio

AAudio non fornisce callback per avvisarti dei cambiamenti di stato. Uno speciale nella funzione di chiamata, È possibile utilizzare AAudioStream_waitForStateChange(stream, inputState, nextState, timeout) per attendere un cambio di stato.

La funzione non rileva da solo un cambiamento di stato e non attende un per ogni stato specifico. Attende che lo stato attuale è diverso da inputState, che hai specificato.

Ad esempio, dopo aver richiesto di mettere in pausa, uno stream deve inserire immediatamente lo stato temporaneo In pausa e arriverà in un secondo momento allo stato In pausa, anche se non c'è alcuna garanzia che lo sia. Poiché non puoi attendere lo stato In pausa, usa waitForStateChange() per attendere qualsiasi stato diverse da "Messa in pausa". Procedi nel seguente modo:

aaudio_stream_state_t inputState = AAUDIO_STREAM_STATE_PAUSING;
aaudio_stream_state_t nextState = AAUDIO_STREAM_STATE_UNINITIALIZED;
int64_t timeoutNanos = 100 * AAUDIO_NANOS_PER_MILLISECOND;
result = AAudioStream_requestPause(stream);
result = AAudioStream_waitForStateChange(stream, inputState, &nextState, timeoutNanos);

Se lo stato dello stream non è In pausa (inputState, che abbiamo presupposto fosse il stato corrente al momento della chiamata), la funzione restituisce immediatamente. Altrimenti, blocchi fino a quando lo stato non è più In pausa o il timeout scade. Quando , il parametro nextState mostra lo stato corrente flusso di dati.

Puoi utilizzare questa stessa tecnica dopo aver chiamato l'avvio, l'arresto o lo svuotamento utilizzando lo stato temporaneo corrispondente come inputState. Non chiamare waitForStateChange() dopo aver chiamato AAudioStream_close() dallo stream verrà eliminato non appena verrà chiuso. E non chiamare AAudioStream_close() mentre waitForStateChange() è in esecuzione in un altro thread.

Lettura e scrittura in uno stream audio

Esistono due modi per elaborare i dati in un flusso dopo il loro avvio:

Per una lettura o scrittura di blocco che trasferisce il numero specificato di frame, imposta timeoutNanos maggiore di zero. Per una chiamata che non blocca, imposta timeoutNanos su zero. In questo caso il risultato è il numero effettivo di frame trasferiti.

Quando leggi l'input, devi verificare il numero corretto di è stato letto. In caso contrario, il buffer potrebbe contenere dati sconosciuti che potrebbero causare un glitch audio. Puoi aggiungere zeri al buffer per creare abbandono silenzioso:

aaudio_result_t result =
    AAudioStream_read(stream, audioData, numFrames, timeout);
if (result < 0) {
  // Error!
}
if (result != numFrames) {
  // pad the buffer with zeros
  memset(static_cast<sample_type*>(audioData) + result * samplesPerFrame, 0,
      sizeof(sample_type) * (numFrames - result) * samplesPerFrame);
}

Puoi preparare il buffer del flusso prima di avviarlo scrivendo dati o silenziando i contenuti. Questa operazione deve essere eseguita in una chiamata non bloccante con timeoutNanos impostato su zero.

I dati nel buffer devono corrispondere al formato dei dati restituito da AAudioStream_getDataFormat().

Chiusura di uno stream audio

Quando hai finito di utilizzare uno stream, chiudilo:

AAudioStream_close(stream);

Una volta chiuso uno stream, non puoi utilizzarlo con alcuna funzione basata su stream AAudio.

Stream audio disconnesso

Uno stream audio può interrompersi in qualsiasi momento se si verifica uno dei seguenti eventi:

  • Il dispositivo audio associato non è più connesso (ad esempio quando le cuffie sono scollegate).
  • Si verifica un errore interno.
  • Un dispositivo audio non è più il dispositivo audio principale.

Lo stato di uno stream scollegato è "Disconnesso" e qualsiasi tentativo di eseguire AAudioStream_write() o altre funzioni restituirà un errore. Devi sempre arrestare e chiudere uno stream disconnesso, indipendentemente dal codice di errore.

Se utilizzi un callback di dati (anziché uno dei metodi di lettura/scrittura diretti) non riceverai alcun codice di ritorno quando lo stream viene disconnesso. Per essere informato in questo caso, scrivi un AAudioStream_errorCallback e la registri utilizzando AAudioStreamBuilder_setErrorCallback().

Se ti viene notificata la disconnessione in un thread di callback di errore, l'arresto e la chiusura del flusso deve essere fatto da un altro thread. In caso contrario, potresti avere un deadlock.

Tieni presente che se apri un nuovo stream, potrebbe avere impostazioni diverse dal flusso originale (ad esempio framePerBurst):

void errorCallback(AAudioStream *stream,
                   void *userData,
                   aaudio_result_t error) {
    // Launch a new thread to handle the disconnect.
    std::thread myThread(my_error_thread_proc, stream, userData);
    myThread.detach(); // Don't wait for the thread to finish.
}

Ottimizzazione del rendimento

Puoi ottimizzare le prestazioni di un'applicazione audio regolando i buffer interni e utilizzando speciali thread ad alta priorità.

Ottimizzazione dei buffer per ridurre al minimo la latenza

Un audio trasmette i dati in entrata e in uscita dai buffer interni che conserva, uno per ogni dispositivo audio.

La capacità del buffer è la quantità totale di dati che il buffer può contenere. Puoi chiamare AAudioStreamBuilder_setBufferCapacityInFrames() per impostare la capacità. Il metodo limita la capacità che puoi allocare al valore massimo consentito dal dispositivo. Utilizza le funzionalità di AAudioStream_getBufferCapacityInFrames() per verificare la capacità effettiva del buffer.

Un'app non deve utilizzare l'intera capacità di un buffer. AAudio riempie il buffer fino a una dimensione che puoi impostare. La dimensione di un buffer non può essere superiore alla sua capacità e spesso è minore. Controllando la dimensione del buffer puoi determinare il numero di burst necessari per riempirlo e quindi controllare la latenza. Utilizza i metodi AAudioStreamBuilder_setBufferSizeInFrames() e AAudioStreamBuilder_getBufferSizeInFrames() per lavorare con la dimensione del buffer.

Quando un'applicazione riproduce l'audio, scrive su un buffer e si blocca fino al completamento della scrittura. AAudio legge dal buffer in burst discrete. Ogni raffica contiene un numero multiplo di fotogrammi audio e di solito è inferiore alle dimensioni del buffer di lettura. Il sistema controlla le dimensioni e la frequenza delle serie di foto a raffica, in genere queste proprietà sono dettate dai circuiti del dispositivo audio. Sebbene non sia possibile modificare le dimensioni o la frequenza delle serie a raffica, puoi impostare le dimensioni del buffer interno in base al numero di burst che contiene. In genere, si ottiene la latenza più bassa se la dimensione del buffer del tuo AAudioStream è un multiplo della dimensione della serie di foto a raffica riportata.

      Buffer Aaudio

Un modo per ottimizzare la dimensione del buffer è iniziare con un buffer di grandi dimensioni e abbassarlo gradualmente fino all'insorgenza di livelli inferiori, quindi riportarlo di nuovo verso l'alto. In alternativa, puoi iniziare con una dimensione del buffer ridotta e, se ciò genera interruzioni, aumentare la dimensione del buffer fino a quando l'output non scorre di nuovo in modo pulito.

Questa procedura può avvenire molto rapidamente, probabilmente prima che l'utente riproduca il primo suono. Ti consigliamo di eseguire prima il dimensionamento del buffer iniziale, utilizzando la modalità silenziosa, in modo che l'utente non possa sentire nessun problema audio. Le prestazioni del sistema potrebbero cambiare nel tempo (ad esempio, l'utente potrebbe disattivare la modalità aereo). Poiché l'ottimizzazione del buffer aggiunge un overhead molto ridotto, la tua app può farlo ininterrottamente mentre l'app legge o scrive i dati in uno stream.

Di seguito è riportato un esempio di loop di ottimizzazione del buffer:

int32_t previousUnderrunCount = 0;
int32_t framesPerBurst = AAudioStream_getFramesPerBurst(stream);
int32_t bufferSize = AAudioStream_getBufferSizeInFrames(stream);

int32_t bufferCapacity = AAudioStream_getBufferCapacityInFrames(stream);

while (go) {
    result = writeSomeData();
    if (result < 0) break;

    // Are we getting underruns?
    if (bufferSize < bufferCapacity) {
        int32_t underrunCount = AAudioStream_getXRunCount(stream);
        if (underrunCount > previousUnderrunCount) {
            previousUnderrunCount = underrunCount;
            // Try increasing the buffer size by one burst
            bufferSize += framesPerBurst;
            bufferSize = AAudioStream_setBufferSize(stream, bufferSize);
        }
    }
}

Non vi è alcun vantaggio nell'utilizzare questa tecnica per ottimizzare la dimensione del buffer per un flusso di input. Gli stream di input vengono eseguiti il più velocemente possibile, cercando di mantenere la quantità di dati presenti nel buffer al minimo per poi riempire quando l'app viene prerilasciata.

Utilizzare un callback ad alta priorità

Se la tua app legge o scrive dati audio da un thread normale, potrebbe essere prerilasciata o presentare un tremolio temporale. Ciò può causare problemi audio. L'utilizzo di buffer più grandi potrebbe prevenire questi glitch, ma un buffer di grandi dimensioni introduce anche una latenza audio più lunga. Per le applicazioni che richiedono una bassa latenza, uno stream audio può utilizzare una funzione di callback asincrono per trasferire dati da e verso la tua app. AAudio esegue il callback in un thread di priorità più elevata con prestazioni migliori.

La funzione di callback ha questo prototipo:

typedef aaudio_data_callback_result_t (*AAudioStream_dataCallback)(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames);

Usa lo sviluppo dello stream per registrare il callback:

AAudioStreamBuilder_setDataCallback(builder, myCallback, myUserData);

Nel caso più semplice, il flusso esegue periodicamente la funzione callback per acquisire i dati per il prossimo burst.

La funzione di callback non deve eseguire una lettura o una scrittura sul flusso che l'ha richiamato. Se il callback appartiene a uno stream di input, il codice dovrebbe elaborare i dati che vengono forniti nel buffer audioData (specificato come ). Se il callback appartiene a uno stream di output, il codice deve inserire i dati nel buffer.

Ad esempio, potresti utilizzare un callback per generare continuamente un'uscita sinusoidale nel seguente modo:

aaudio_data_callback_result_t myCallback(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames) {
    int64_t timeout = 0;

    // Write samples directly into the audioData array.
    generateSineWave(static_cast<float *>(audioData), numFrames);
    return AAUDIO_CALLABCK_RESULT_CONTINUE;
}

Con AAudio è possibile elaborare più stream. Puoi usare uno stream come principale e trasferire i cursori ad altri flussi nei dati utente. Registra un callback per lo stream principale. Quindi, utilizza I/O senza blocchi sugli altri stream. Ecco un esempio di callback andata e ritorno che passa un flusso di input a uno stream di output. Il flusso della chiamata principale è il flusso di output. Lo stream di input è incluso nei dati utente.

Il callback esegue una lettura non bloccante dal flusso di input che inserisce i dati nel buffer del flusso di output:

aaudio_data_callback_result_t myCallback(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames) {
    AAudioStream *inputStream = (AAudioStream *) userData;
    int64_t timeout = 0;
    aaudio_result_t result =
        AAudioStream_read(inputStream, audioData, numFrames, timeout);

  if (result == numFrames)
      return AAUDIO_CALLABCK_RESULT_CONTINUE;
  if (result >= 0) {
      memset(static_cast<sample_type*>(audioData) + result * samplesPerFrame, 0,
          sizeof(sample_type) * (numFrames - result) * samplesPerFrame);
      return AAUDIO_CALLBACK_RESULT_CONTINUE;
  }
  return AAUDIO_CALLBACK_RESULT_STOP;
}

Tieni presente che in questo esempio si presuppone che gli stream di input e output abbiano lo stesso numero di canali, formato e frequenza di campionamento. Il formato degli stream può non corrispondere, purché il codice gestisca correttamente le traduzioni.

Impostare la modalità prestazioni

Ogni AAudioStream dispone di una modalità prestazioni che influisce notevolmente sul comportamento della tua app. Esistono tre modalità:

  • AAUDIO_PERFORMANCE_MODE_NONE è la modalità predefinita. Utilizza uno stream di base che bilancia la latenza e il risparmio energetico.
  • AAUDIO_PERFORMANCE_MODE_LOW_LATENCY utilizza buffer più piccoli e un percorso dati ottimizzato per una latenza ridotta.
  • AAUDIO_PERFORMANCE_MODE_POWER_SAVING utilizza buffer interni più grandi e un percorso dati che bilancia la latenza con una potenza inferiore.

Puoi selezionare la modalità prestazioni chiamando setPerformanceMode(), e scopri la modalità attuale chiamando getPerformanceMode().

Se la bassa latenza è più importante del risparmio energetico nell'applicazione, utilizza AAUDIO_PERFORMANCE_MODE_LOW_LATENCY. Ciò è utile per le app molto interattive, come i giochi o i sintetizzatori da tastiera.

Se il risparmio energetico è più importante della bassa latenza nell'applicazione, utilizza AAUDIO_PERFORMANCE_MODE_POWER_SAVING. Questo è tipico per le app che riproducono musica generata in precedenza, come l'audio in streaming o i lettori di file MIDI.

Nella versione attuale di AAudio, per ottenere la latenza più bassa possibile, devi utilizzare la modalità prestazioni AAUDIO_PERFORMANCE_MODE_LOW_LATENCY insieme a un callback ad alta priorità. Segui questo esempio:

// Create a stream builder
AAudioStreamBuilder *streamBuilder;
AAudio_createStreamBuilder(&streamBuilder);
AAudioStreamBuilder_setDataCallback(streamBuilder, dataCallback, nullptr);
AAudioStreamBuilder_setPerformanceMode(streamBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);

// Use it to create the stream
AAudioStream *stream;
AAudioStreamBuilder_openStream(streamBuilder, &stream);

Sicurezza dei thread

L'API AAudio non è completamente a sicurezza tramite thread. Non puoi chiamare contemporaneamente alcune funzioni AAudio da più thread alla volta. Questo accade perché AAudio evita l'uso dei mutex, il che può causare il prerilascio dei thread e gli glitch.

Per sicurezza, non chiamare AAudioStream_waitForStateChange() e non leggere o scrivere nello stesso stream da due thread diversi. Allo stesso modo, non chiudere un flusso in un thread mentre leggi o scrivi in un altro thread.

Le chiamate che restituiscono impostazioni di streaming, come AAudioStream_getSampleRate() e AAudioStream_getChannelCount(), sono sicure in thread.

Anche queste chiamate sono protette da thread:

  • AAudio_convert*ToText()
  • AAudio_createStreamBuilder()
  • AAudioStream_get*() tranne AAudioStream_getTimestamp()
di Gemini Advanced.

Problemi noti

  • La latenza audio è elevata per il blocco di write() perché la release Android O DP2 non utilizza una traccia FAST. Utilizza un callback per ridurre la latenza.

Risorse aggiuntive

Per saperne di più, consulta le seguenti risorse:

API Reference

Codelab

Video