# Integración móvil | Docs de AudioDN

> Integra AudioDN en tu app iOS, Android o Flutter usando llamadas nativas a la API con claves API Client-Side.

Source: https://audiodeliverynetwork.com/es/docs/integration/mobile/

---

# Integración móvil

Usa la API de ADN con claves API Client-Side desde tu app iOS, Android o Flutter. Reproduce y sube archivos con elementos multimedia nativos.

#### Ideal para

Apps iOS, Android y Flutter. La configuración tarda unos 30 minutos.

## Reproducir pistas

Crea sesiones de reproducción y reproduce audio con players multimedia nativos.

Llama a la API de reproducción de ADN directamente desde tu app móvil usando una clave API Client-Side. Crea sesiones de reproducción, obtén URLs de variante firmadas y reproduce audio con players multimedia nativos.

### 1\. Crear una clave API Client-Side Player

En **Settings → API Keys**, crea una clave **Client-Side Player**. Limítala a las variantes y el contenido al que tu app móvil debería acceder.

### 2\. Crear una sesión de reproducción

  

```
import Foundation

struct PlaySessionRequest: Encodable {
    let collectionId: String
    let variants: [String]
    let isDownloadable: Bool
    let expiresIn: Int
    enum CodingKeys: String, CodingKey {
        case collectionId = "collection_id"; case variants
        case isDownloadable = "is_downloadable"; case expiresIn = "expires_in"
    }
}
struct PlaySessionResponse: Decodable {
    let playSessionId: String
    let tracks: [Track]
    enum CodingKeys: String, CodingKey { case playSessionId = "play_session_id"; case tracks }
    struct Track: Decodable { let id: String }
}

func createPlaySession() async throws -> (playSessionId: String, tracks: [PlaySessionResponse.Track]) {
    var request = URLRequest(url: URL(string: "https://api.audiodelivery.net/v1/play_session/collection")!)
    request.httpMethod = "POST"
    request.setValue("Bearer CLIENT-SIDE-PLAYER-API-KEY", forHTTPHeaderField: "Authorization")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    let encoder = JSONEncoder()
    encoder.keyEncodingStrategy = .convertToSnakeCase
    request.httpBody = try encoder.encode(PlaySessionRequest(
        collectionId: "COLLECTION-ID", variants: ["hq", "lq"], isDownloadable: false, expiresIn: 3600))
    let (data, _) = try await URLSession.shared.data(for: request)
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let session = try decoder.decode(PlaySessionResponse.self, from: data)
    return (session.playSessionId, session.tracks)
}
```

```
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody

@Serializable
data class PlaySessionRequest(val collection_id: String, val variants: List<String>,
    val is_downloadable: Boolean, val expires_in: Int)
@Serializable
data class PlaySessionResponse(val play_session_id: String, val tracks: List<PlaySessionTrack>) {
    @Serializable data class PlaySessionTrack(val id: String)
}

val client = OkHttpClient()
val json = Json { ignoreUnknownKeys = true }

suspend fun createPlaySession(): PlaySessionResponse = withContext(Dispatchers.IO) {
    val body = json.encodeToString(PlaySessionRequest.serializer(), PlaySessionRequest(
        "COLLECTION-ID", listOf("hq", "lq"), false, 3600)).toRequestBody("application/json".toMediaType())
    val request = Request.Builder()
        .url("https://api.audiodelivery.net/v1/play_session/collection")
        .post(body)
        .addHeader("Authorization", "Bearer CLIENT-SIDE-PLAYER-API-KEY")
        .build()
    val response = client.newCall(request).execute()
    val responseBody = response.body?.string() ?: error("Empty response body")
    json.decodeFromString<PlaySessionResponse>(responseBody)
}
```

```
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<({String playSessionId, List<dynamic> tracks})> createPlaySession() async {
  final response = await http.post(
    Uri.parse('https://api.audiodelivery.net/v1/play_session/collection'),
    headers: {
      'Authorization': 'Bearer CLIENT-SIDE-PLAYER-API-KEY',
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'collection_id': 'COLLECTION-ID',
      'variants': ['hq', 'lq'],
      'is_downloadable': false,
      'expires_in': 3600,
    }),
  );
  if (response.statusCode < 200 || response.statusCode >= 300) {
    throw Exception('Play session failed: ${response.statusCode}');
  }
  final json = jsonDecode(response.body) as Map<String, dynamic>;
  return (
    playSessionId: json['play_session_id'] as String,
    tracks: json['tracks'] as List<dynamic>,
  );
}
```

### 3\. Obtener URLs de variante de pista

  

```
struct TrackVariantsResponse: Decodable {
    let variants: [Variant]
    struct Variant: Decodable {
        let url: String
        let variant: VariantIndex
        struct VariantIndex: Decodable { let id: String }
    }
}

// No auth needed — the session ID acts as a bearer token
func getAudioUrl(playSessionId: String, trackId: String) async throws -> String {
    let url = URL(string: "https://api.audiodelivery.net/v1/play/\(playSessionId)/\(trackId)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let res = try decoder.decode(TrackVariantsResponse.self, from: data)
    let hq = res.variants.first { $0.variant.id == "hq" }!
    return hq.url
}
```

```
@Serializable
data class TrackVariantsResponse(val variants: List<Variant>) {
    @Serializable data class Variant(val url: String, val variant: VariantIndex) {
        @Serializable data class VariantIndex(val id: String)
    }
}

// No auth needed — the session ID acts as a bearer token
suspend fun getAudioUrl(playSessionId: String, trackId: String): String = withContext(Dispatchers.IO) {
    val request = Request.Builder()
        .url("https://api.audiodelivery.net/v1/play/$playSessionId/$trackId")
        .get()
        .build()
    val response = client.newCall(request).execute()
    val responseBody = response.body?.string() ?: error("Empty response body")
    val res = json.decodeFromString<TrackVariantsResponse>(responseBody)
    res.variants.first { it.variant.id == "hq" }.url
}
```

```
import 'dart:convert';
import 'package:http/http.dart' as http;

// No auth needed — the session ID acts as a bearer token
Future<String> getAudioUrl(String playSessionId, String trackId) async {
  final response = await http.get(
    Uri.parse('https://api.audiodelivery.net/v1/play/$playSessionId/$trackId'),
  );
  if (response.statusCode < 200 || response.statusCode >= 300) {
    throw Exception('Fetch track failed: ${response.statusCode}');
  }
  final json = jsonDecode(response.body) as Map<String, dynamic>;
  final variants = json['variants'] as List<dynamic>;
  final hqVariant = variants.cast<Map<String, dynamic>>().firstWhere(
    (v) => (v['variant'] as Map<String, dynamic>)['id'] == 'hq',
    orElse: () => throw StateError('HQ variant not found'),
  );
  return hqVariant['url'] as String;
}
```

### 4\. Reproducir con el player nativo

  

```
import AVFoundation

let player = AVPlayer(url: URL(string: audioUrl)!)
player.play()
```

```
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer

val player = ExoPlayer.Builder(context).build()
val mediaItem = MediaItem.fromUri(audioUrl)
player.setMediaItem(mediaItem)
player.prepare()
player.play()
```

```
import 'package:just_audio/just_audio.dart';

final player = AudioPlayer();
await player.setUrl(audioUrl);
await player.play();
// Dispose when done: player.dispose();
```

La URL firmada funciona con cualquier player multimedia nativo. ADN gestiona la entrega CDN y la verificación de URL.

#### Datos de forma de onda

ADN genera automáticamente datos de forma de onda RMS normalizados (320 muestras) para cada pista. El campo `levels` en la respuesta de la pista contiene los datos — úsalos para renderizar formas de onda en tu propio player, o genera formas de onda personalizadas mediante el tipo de variante Audio Analysis.

#### Imágenes de portada y colores de tema

ADN analiza cada pista subida en busca de arte de portada incrustado y lo pone disponible como `cover_image` en la respuesta de la pista (en tamaños icon/small/regular/large). También puedes subir una imagen de portada por separado vía la API. Cuando hay una imagen de portada, ADN extrae una paleta de colores y selecciona un `player_color` principal — úsalos para estilizar la UI de tu player. Junto a él, las respuestas incluyen `player_color_light` y `player_color_dark`, variantes ajustadas por contraste para dibujar el color sobre fondos claros y oscuros respectivamente. El array completo `theme` contiene todos los colores extraídos con valores hex, área, luminosidad y saturación.

## Subir pistas

Sube archivos de audio desde apps nativas usando la API de subida de ADN.

Llama a la API de subida de ADN directamente desde tu app móvil usando una clave API Client-Side. Crea sesiones de subida, obtén URLs prefirmadas y sube archivos de audio — todo desde código nativo.

### 1\. Crear una clave API Client-Side Upload

Inicia sesión en ADN, ve a **Settings → API Keys** y crea una clave **Client-Side Upload**. Limítala a la colección a la que tu app debería subir.

### 2\. Crear una sesión de subida

  

```
import Foundation

struct UploadSessionRequest: Encodable {
    let collectionId: String
    let expiresIn: Int
    enum CodingKeys: String, CodingKey { case collectionId = "collection_id"; case expiresIn = "expires_in" }
}
struct UploadSessionResponse: Decodable {
    let uploadSession: UploadSession
    enum CodingKeys: String, CodingKey { case uploadSession = "upload_session" }
    struct UploadSession: Decodable { let id: String }
}

func createUploadSession() async throws -> String {
    var request = URLRequest(url: URL(string: "https://api.audiodelivery.net/v1/upload_session")!)
    request.httpMethod = "POST"
    request.setValue("Bearer CLIENT-SIDE-UPLOAD-API-KEY", forHTTPHeaderField: "Authorization")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    let encoder = JSONEncoder()
    encoder.keyEncodingStrategy = .convertToSnakeCase
    request.httpBody = try encoder.encode(UploadSessionRequest(collectionId: "COLLECTION-ID", expiresIn: 3600))
    let (data, _) = try await URLSession.shared.data(for: request)
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let session = try decoder.decode(UploadSessionResponse.self, from: data)
    return session.uploadSession.id
}
```

```
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody

@Serializable
data class UploadSessionRequest(val collection_id: String, val expires_in: Int)
@Serializable
data class UploadSessionResponse(val upload_session: UploadSession) {
    @Serializable data class UploadSession(val id: String)
}

val client = OkHttpClient()
val json = Json { ignoreUnknownKeys = true }

suspend fun createUploadSession(): String = withContext(Dispatchers.IO) {
    val body = json.encodeToString(UploadSessionRequest.serializer(),
        UploadSessionRequest("COLLECTION-ID", 3600)).toRequestBody("application/json".toMediaType())
    val request = Request.Builder()
        .url("https://api.audiodelivery.net/v1/upload_session")
        .post(body)
        .addHeader("Authorization", "Bearer CLIENT-SIDE-UPLOAD-API-KEY")
        .build()
    val response = client.newCall(request).execute()
    val responseBody = response.body?.string() ?: error("Empty response body")
    val session = json.decodeFromString<UploadSessionResponse>(responseBody)
    session.upload_session.id
}
```

```
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<String> createUploadSession() async {
  final response = await http.post(
    Uri.parse('https://api.audiodelivery.net/v1/upload_session'),
    headers: {
      'Authorization': 'Bearer CLIENT-SIDE-UPLOAD-API-KEY',
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'collection_id': 'COLLECTION-ID',
      'expires_in': 3600,
    }),
  );
  if (response.statusCode < 200 || response.statusCode >= 300) {
    throw Exception('Upload session failed: ${response.statusCode}');
  }
  final json = jsonDecode(response.body) as Map<String, dynamic>;
  return (json['upload_session'] as Map<String, dynamic>)['id'] as String;
}
```

### 3\. Obtener una URL de subida de pista

  

```
struct TrackUploadRequest: Encodable {
    let fileName: String
    enum CodingKeys: String, CodingKey { case fileName = "file_name" }
}
struct TrackUploadResponse: Decodable {
    let trackUpload: TrackUpload
    enum CodingKeys: String, CodingKey { case trackUpload = "track_upload" }
    struct TrackUpload: Decodable { let uploadUrl: String; let method: String
        enum CodingKeys: String, CodingKey { case uploadUrl = "upload_url"; case method }
    }
}

func getTrackUploadUrl(sessionId: String) async throws -> (url: String, method: String) {
    var request = URLRequest(url: URL(string: "https://api.audiodelivery.net/v1/upload/\(sessionId)/track")!)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    let encoder = JSONEncoder()
    encoder.keyEncodingStrategy = .convertToSnakeCase
    request.httpBody = try encoder.encode(TrackUploadRequest(fileName: "recording.m4a"))
    let (data, _) = try await URLSession.shared.data(for: request)
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let res = try decoder.decode(TrackUploadResponse.self, from: data)
    return (res.trackUpload.uploadUrl, res.trackUpload.method)
}
```

```
@Serializable
data class TrackUploadRequest(val file_name: String)
@Serializable
data class TrackUploadResponse(val track_upload: TrackUpload) {
    @Serializable data class TrackUpload(val upload_url: String, val method: String)
}

suspend fun getTrackUploadUrl(uploadSessionId: String): Pair<String, String> = withContext(Dispatchers.IO) {
    val body = json.encodeToString(TrackUploadRequest.serializer(), TrackUploadRequest("recording.m4a"))
        .toRequestBody("application/json".toMediaType())
    val request = Request.Builder()
        .url("https://api.audiodelivery.net/v1/upload/$uploadSessionId/track")
        .post(body)
        .build()
    val response = client.newCall(request).execute()
    val responseBody = response.body?.string() ?: error("Empty response body")
    val res = json.decodeFromString<TrackUploadResponse>(responseBody)
    res.track_upload.upload_url to res.track_upload.method
}
```

```
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<(String, String)> getTrackUploadUrl(String uploadSessionId) async {
  final response = await http.post(
    Uri.parse('https://api.audiodelivery.net/v1/upload/$uploadSessionId/track'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({'file_name': 'recording.m4a'}),
  );
  if (response.statusCode < 200 || response.statusCode >= 300) {
    throw Exception('Track upload URL failed: ${response.statusCode}');
  }
  final json = jsonDecode(response.body) as Map<String, dynamic>;
  final trackUpload = json['track_upload'] as Map<String, dynamic>;
  return (trackUpload['upload_url'] as String, trackUpload['method'] as String);
}
```

### 4\. Subir el archivo

  

```
// Prefer streaming for large files instead of loading into memory
func uploadAudio(fileURL: URL, uploadURL: URL, method: String) async throws {
    var request = URLRequest(url: uploadURL)
    request.httpMethod = method
    request.setValue("audio/mp4", forHTTPHeaderField: "Content-Type")
    request.httpBodyStream = InputStream(url: fileURL)
    let (_, uploadResponse) = try await URLSession.shared.data(for: request)
    let httpResponse = uploadResponse as! HTTPURLResponse
    print("Upload status: \(httpResponse.statusCode)") // 200 on success
}
```

```
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File

suspend fun uploadFile(uploadUrl: String, filePath: String): Int = withContext(Dispatchers.IO) {
    val file = File(filePath)
    val fileBody = file.asRequestBody("audio/mp4".toMediaType())
    val request = Request.Builder()
        .url(uploadUrl)
        .put(fileBody)
        .addHeader("Content-Type", "audio/mp4")
        .build()
    val response = client.newCall(request).execute()
    response.code
}
```

```
import 'dart:io';
import 'package:http/http.dart' as http;

// For large files, use body: File(path).openRead() to stream instead of loading into memory
Future<void> uploadFile(String uploadUrl, String filePath) async {
  final file = File(filePath);
  final response = await http.put(
    Uri.parse(uploadUrl),
    headers: {'Content-Type': 'audio/mp4'},
    body: await file.readAsBytes(),
  );
  if (response.statusCode < 200 || response.statusCode >= 300) {
    throw Exception('Upload failed: ${response.statusCode}');
  }
}
```

Repite los pasos 3 y 4 por cada archivo — cada pista necesita su propia solicitud de subida y URL firmada. También puedes incluir un objeto anidado `track` al crear la sesión para crear la primera pista de antemano, y luego usar el paso 3 para cualquier archivo adicional.

### 5\. Esperar a que la pista esté lista

El procesamiento comienza automáticamente después de la subida. Antes de reproducir la pista, espera hasta que esté lista — consulta `GET /v1/track/{track_id}` hasta que `track.track_status_id` sea `ready`, o configura un webhook (paso siguiente) para recibir notificaciones en lugar de hacer polling.

### 6\. Opcional: Webhooks

Añade un webhook en **Settings → Webhook** para recibir notificaciones cuando una pista alcanza un resultado terminal (`ready`, `incomplete`, `error` o `init_error`) y cuando su conjunto completo de archivos está listo. Consulta la documentación del [webhook Track Processing](/docs/webhooks/track-processing).

#### Imágenes de portada

ADN analiza automáticamente las pistas subidas en busca de arte de portada incrustado. También puedes subir una imagen de portada por separado vía la Covers API. Cuando hay una imagen, ADN extrae colores (disponibles como `theme`) que se pueden usar para estilizar la UI de tu player.
