Skip to main content
VESDK/iOS/Guides/Audio Overlays/Custom Libraries

External Audio Provider

VideoEditor SDK for iOS allows integration with external APIs as a data source for custom audio clip categories

External audio provider#

External APIs (audio search engines) can be used as a data source for custom audio clip categories. The provider object must conform to AudioProvider, by implementing a general trending endpoint for the initial load, a search endpoint for queries as well as a get endpoint for retrieving individual songs.

Pagination is handled via the query, offset, and AudioProviderResult.hasMore parameters. If your service does not support pagination, you can ignore the query, offset arguments and pass false as AudioProviderResult.hasMore.

class ExternalAudioProvider: AudioProvider {
func trending(offset: Int, limit: Int, completion: @escaping (Result<AudioProviderResult, Error>) -> Void) {
// Load audio clips from local/remote resource and call completion handler
}
func search(query: String, offset: Int, limit: Int, completion: @escaping (Result<AudioProviderResult, Error>) -> Void) {
// Load audio clips from local/remote resource and call completion handler
}
func get(identifier: String, completion: @escaping (Result<AudioProviderResult, Error>) -> Void) {
// Load an audio clip from local/remote resource and call completion handler
}
}

A custom category can be added to the existing categories in a familiar way. Instead of passing an audioClips array, you should provide an audioProvider object.

let previewURL = Bundle.main.url(forResource: "external_provider", withExtension: "png")!
let customAudioCategory = AudioProviderCategory(title: "ExternalProvider", imageURL: previewURL, audioProvider: ExternalAudioProvider())
let configuration = Configuration { builder in
let assetCatalog = AssetCatalog.defaultItems
assetCatalog.audioClips.append(customAudioCategory)
builder.assetCatalog = assetCatalog
}

Example audio provider#

The example below shows an implementation of an AudioProvider for an arbitrary web service.

class WebServiceAudioProvider: AudioProvider {
private static let baseURL = "https://web.service/api/audio"
private enum Parameters: String {
case query = "q"
}
func trending(offset: Int, limit: Int, completion: @escaping (Result<AudioProviderResult, Error>) -> Void) {
let task = URLSession.shared.dataTask(with: request(path: "trending", query: nil)) { data, response, error in
self.parse(data: data, response: response, error: error, completion: completion)
}
task.resume()
}
func search(query: String, offset: Int, limit: Int, completion: @escaping (Result<AudioProviderResult, Error>) -> Void) {
let task = URLSession.shared.dataTask(with: request(path: "search", query: nil)) { data, response, error in
self.parse(data: data, response: response, error: error, completion: completion)
}
task.resume()
}
func get(identifier: String, completion: @escaping (Result<AudioProviderResult, Error>) -> Void) {
let task = URLSession.shared.dataTask(with: requestSong(identifier: identifier)) { data, response, error in
self.parseSong(data: data, response: response, error: error, completion: completion)
}
task.resume()
}
private func request(path: String, query: String?) -> URLRequest {
var urlComponents = URLComponents(string: WebServiceAudioProvider.baseURL)!
if let query = query {
urlComponents.queryItems?.append(URLQueryItem(name: Parameters.query.rawValue, value: query))
}
let request = URLRequest(url: urlComponents.url!.appendingPathComponent(path))
return request
}
private func requestSong(identifier: String) -> URLRequest {
let url = URL(string: WebServiceAudioProvider.baseURL)!.appendingPathComponent(identifier)
let request = URLRequest(url: url)
return request
}
private func parse(data: Data?, response: URLResponse?, error: Error?, completion: @escaping (Result<AudioProviderResult, Error>) -> Void) {
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.success(AudioProviderResult(audioClips: [])))
return
}
do {
let response = try JSONDecoder().decode(WebServiceAudioResponse.self, from: data)
let result = AudioProviderResult(response: response)
completion(.success(result))
} catch {
completion(.failure(error))
}
}
private func parseSong(data: Data?, response: URLResponse?, error: Error?, completion: @escaping (Result<AudioProviderResult, Error>) -> Void) {
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.success(AudioProviderResult(audioClips: [])))
return
}
do {
let response = try JSONDecoder().decode(WebServiceAudioClip.self, from: data)
let audioClip = AudioClip(audio: response)
let result = AudioProviderResult(audioClips: [audioClip].compactMap { $0 })
completion(.success(result))
} catch {
completion(.failure(error))
}
}
}
struct WebServiceAudioClip: Decodable {
let id: String
let url: URL
}
struct WebServiceAudioResponse: Decodable {
let songs: [WebServiceAudioClip]
}
extension AudioClip {
convenience init(audio: WebServiceAudioClip) {
self.init(identifier: audio.id, audioURL: audio.url, resolver: WebServiceAssetResovler.identifier)
}
}
extension AudioProviderResult {
convenience init(response: WebServiceAudioResponse) {
let audioClips = response.songs.map(AudioClip.init)
self.init(audioClips: audioClips)
}
}
class WebServiceAssetResovler: AssetResolver {
/// The identifier of this resolver.
static let identifier = "web_service_resolver"
/// The `WebServiceAudioProvider` responsible for fetching the song for deserialization.
public let provider: WebServiceAudioProvider
/// Initializes a new `WebServiceAudioProvider` from a provider.
///
/// - Parameters:
/// - provider: The `WebServiceAudioProvider` responsible for fetching the song for deserialization.
public required init(provider: WebServiceAudioProvider) {
self.provider = provider
}
func deserialize(from data: [String: String], completion: @escaping (ResolvableAsset?) -> Void) {
guard let id = data["id"] else {
completion(nil)
return
}
provider.get(identifier: id) { result in
switch result {
case .success(let result):
if let audioClip = result.audioClips.first {
completion(audioClip)
} else {
completion(nil)
}
return
case .failure:
completion(nil)
return
}
}
}
func serialize(_ asset: ResolvableAsset) -> [String: String]? {
if let audioClip = asset as? AudioClip {
return ["id": audioClip.identifier]
}
return nil
}
}