# Mobile-Integration | AudioDN Docs

> Integrieren Sie AudioDN in Ihre iOS-, Android- oder Flutter-App mit nativen API-Aufrufen und Client-Side-API-Keys.

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

---

# Mobile-Integration

Nutzen Sie die ADN-API mit Client-Side-API-Keys aus Ihrer iOS-, Android- oder Flutter-App. Wiedergabe und Upload mit nativen Medienelementen.

#### Ideal für

iOS-, Android- und Flutter-Apps. Die Einrichtung dauert etwa 30 Minuten.

## Tracks abspielen

Erstellen Sie Wiedergabesessions und streamen Sie Audio mit nativen Medienplayern.

Rufen Sie die Wiedergabe-API von ADN direkt aus Ihrer mobilen App mit einem Client-Side-API-Key auf. Erstellen Sie Wiedergabesessions, rufen Sie signierte Varianten-URLs ab und streamen Sie Audio mit nativen Medienplayern.

### 1\. Client-Side-Player-API-Key erstellen

Erstellen Sie unter **Settings → API Keys** einen **Client-Side Player**\-Key. Beschränken Sie ihn auf die Varianten und Inhalte, auf die Ihre mobile App zugreifen soll.

### 2\. Wiedergabesession erstellen

  

```
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\. Track-Varianten-URLs abrufen

  

```
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\. Mit nativem Player abspielen

  

```
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();
```

Die signierte URL funktioniert mit jedem nativen Medienplayer. ADN übernimmt die CDN-Auslieferung und die URL-Verifizierung.

#### Wellenform-Daten

ADN erzeugt automatisch normalisierte RMS-Wellenform-Daten (320 Samples) für jeden Track. Das Feld `levels` in der Track-Antwort enthält die Daten — nutzen Sie sie, um Wellenformen in Ihrem eigenen Player darzustellen, oder erzeugen Sie individuelle Wellenformen über den Audio-Analyse-Variantentyp.

#### Coverbilder & Theme-Farben

ADN scannt jeden hochgeladenen Track auf eingebettetes Cover-Artwork und stellt es als `cover_image` in der Track-Antwort bereit (in den Größen icon/small/regular/large). Sie können ein Coverbild auch separat über die API hochladen. Wenn ein Coverbild vorhanden ist, extrahiert ADN eine Farbpalette und wählt eine primäre `player_color` — nutzen Sie diese, um Ihre Player-UI zu stylen. Zusätzlich enthalten Antworten `player_color_light` und `player_color_dark`, kontrastangepasste Varianten zum Darstellen der Farbe auf hellem bzw. dunklem Hintergrund. Das vollständige `theme`\-Array enthält alle extrahierten Farben mit Hex-Werten, Fläche, Helligkeit und Sättigungsdaten.

## Tracks hochladen

Laden Sie Audiodateien aus nativen Apps mit der Upload-API von ADN hoch.

Rufen Sie die Upload-API von ADN direkt aus Ihrer mobilen App mit einem Client-Side-API-Key auf. Erstellen Sie Upload-Sessions, erhalten Sie vorsignierte URLs und laden Sie Audiodateien hoch — alles aus nativem Code.

### 1\. Client-Side-Upload-API-Key erstellen

Melden Sie sich bei ADN an, gehen Sie zu **Settings → API Keys** und erstellen Sie einen **Client-Side Upload**\-Key. Beschränken Sie ihn auf die Sammlung, in die Ihre App hochladen soll.

### 2\. Upload-Session erstellen

  

```
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\. Track-Upload-URL abrufen

  

```
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\. Datei hochladen

  

```
// 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}');
  }
}
```

Wiederholen Sie Schritt 3 und 4 für jede Datei — jeder Track benötigt seine eigene Upload-Anfrage und seine eigene signierte URL. Sie können auch ein verschachteltes `track`\-Objekt bei der Session-Erstellung mitsenden, um den ersten Track direkt zu erstellen, und dann Schritt 3 für weitere Dateien nutzen.

### 5\. Warten, bis der Track bereit ist

Die Verarbeitung beginnt automatisch nach dem Upload. Warten Sie, bevor Sie den Track abspielen, bis er bereit ist — fragen Sie `GET /v1/track/{track_id}` ab, bis `track.track_status_id` den Wert `ready` hat, oder konfigurieren Sie einen Webhook (nächster Schritt), um statt Polling benachrichtigt zu werden.

### 6\. Optional: Webhooks

Fügen Sie unter **Settings → Webhook** einen Webhook hinzu, um benachrichtigt zu werden, wenn ein Track ein terminales Ergebnis erreicht (`ready`, `incomplete`, `error` oder `init_error`) und wenn sein vollständiger Dateisatz fertig ist. Siehe die Docs zum [Track-Processing-Webhook](/docs/webhooks/track-processing).

#### Coverbilder

ADN scannt hochgeladene Tracks automatisch auf eingebettetes Cover-Artwork. Sie können ein Coverbild auch separat über die Covers-API hochladen. Wenn ein Bild vorhanden ist, extrahiert ADN Farben (verfügbar als `theme`), die zum Stylen Ihrer Player-UI verwendet werden können.
