Uploading content to MYO cards
1. Request Upload URL
Section titled “1. Request Upload URL”First, get request a secure URL via the /uploadUrl
endpoint. This is a temporary URL on our servers where you’ll upload the audio file. This URL is secure and specific to this upload:
const uploadUrlResponse = await fetch( "https://api.yotoplay.com/media/transcode/audio/uploadUrl", { method: "GET", headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json", }, });
const { upload: { uploadUrl: audioUploadUrl, uploadId },} = await uploadUrlResponse.json();
if (!audioUploadUrl) { throw new Error("Failed to get upload URL");}
The response gives us an upload
object with the following properties:
uploadUrl
: The URL where we’ll upload our audio fileuploadId
: A unique identifier we’ll use to check the transcoding status of the audio file.
2. Upload your audio file to the URL
Section titled “2. Upload your audio file to the URL”Now you can upload you actual audio file to the URL we received with a PUT
request:
await fetch(audioUploadUrl, { method: "PUT", body: new Blob([audioFile], { type: audioFile.type, ContentDisposition: audioFile.name, }), headers: { "Content-Type": audioFile.type, },});
Make sure to set the Content-Type
header to match the type of your audio file.
3. Wait for transcoding
Section titled “3. Wait for transcoding”After upload, Yoto needs to transcode the audio to make it compatible with our Yoto players. We need to keep polling the API until the process is complete:
let transcodedAudio = null;let attempts = 0;const maxAttempts = 30;
while (attempts < maxAttempts) { const transcodeResponse = await fetch( `https://api.yotoplay.com/media/upload/${uploadId}/transcoded?loudnorm=false`, { headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json", }, } );
if (transcodeResponse.ok) { const data = await transcodeResponse.json();
if (data.transcode.transcodedSha256) { transcodedAudio = data.transcode; break; } }
await new Promise((resolve) => setTimeout(resolve, 500)); attempts++;}
if (!transcodedAudio) { throw new Error("Transcoding timed out");}
We know that the transcoding is complete when we receive a transcodedSha256
in the response. This is a hash of the transcoded audio file. We’ll use this value to put our audio file in a track.
4. Create Playlist Content
Section titled “4. Create Playlist Content”We add our audio file to a track, using our transcoded value, and insert that track into a chapter. This creates a new playlist with your audio content.
// Get media info from the transcoded audioconst mediaInfo = transcodedAudio.transcodedInfo;const chapterTitle = mediaInfo?.metadata?.title || title;
const chapters = [ { key: "01", title: chapterTitle, overlayLabel: "1", tracks: [ { key: "01", title: chapterTitle, trackUrl: `yoto:#${transcodedAudio.transcodedSha256}`, duration: mediaInfo?.duration, fileSize: mediaInfo?.fileSize, channels: mediaInfo?.channels, format: mediaInfo?.format, type: "audio", overlayLabel: "1", display: { icon16x16: "yoto:#aUm9i3ex3qqAMYBv-i-O-pYMKuMJGICtR3Vhf289u2Q", }, }, ], display: { icon16x16: "yoto:#aUm9i3ex3qqAMYBv-i-O-pYMKuMJGICtR3Vhf289u2Q", }, },];
// Create the complete content object for a new playlistconst content = { title: title, content: { chapters, }, metadata: { media: { duration: mediaInfo?.duration, fileSize: mediaInfo?.fileSize, readableFileSize: Math.round((mediaInfo?.fileSize / 1024 / 1024) * 10) / 10, }, },};
const createResponse = await fetch("https://api.yotoplay.com/content", { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify(content),});
if (!createResponse.ok) { const errorText = await createResponse.text(); throw new Error(`Failed to create playlist: ${errorText}`);}
const result = await createResponse.json();
The process is complete when this final request returns successfully. Your audio should now appear in your library as a new playlist. You can now link this playlist to a Make Your Own (MYO) card via your Yoto player or the Yoto app.
Here’s the complete code:
export const uploadToCard = async ({ audioFile, title, accessToken }) => { // Step 1: Get upload URL for audio with SHA256 const uploadUrlResponse = await fetch( "https://api.yotoplay.com/media/transcode/audio/uploadUrl", { method: "GET", headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json", }, } );
const { upload: { uploadUrl: audioUploadUrl, uploadId }, } = await uploadUrlResponse.json();
if (!audioUploadUrl) { throw new Error("Failed to get upload URL"); }
// Step 2: Upload the audio file console.log("Progress: uploading - 0%");
await fetch(audioUploadUrl, { method: "PUT", body: new Blob([audioFile], { type: audioFile.type, ContentDisposition: audioFile.name, }), headers: { "Content-Type": audioFile.type, }, });
console.log("Progress: transcoding - 50%");
// Step 3: Wait for transcoding (with timeout) let transcodedAudio = null; let attempts = 0; const maxAttempts = 30;
while (attempts < maxAttempts) { const transcodeResponse = await fetch( `https://api.yotoplay.com/media/upload/${uploadId}/transcoded?loudnorm=false`, { headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json", }, } );
if (transcodeResponse.ok) { const data = await transcodeResponse.json();
if (data.transcode.transcodedSha256) { console.log("Transcoded audio:", data.transcode); transcodedAudio = data.transcode; break; } }
await new Promise((resolve) => setTimeout(resolve, 500)); attempts++; console.log( `Progress: transcoding - ${50 + (attempts / maxAttempts) * 25}%` ); }
if (!transcodedAudio) { throw new Error("Transcoding timed out"); }
// Get media info from the transcoded audio const mediaInfo = transcodedAudio.transcodedInfo;
console.log("Media info:", mediaInfo);
// Step 4: Create new card content console.log("Progress: creating_card - 85%");
const chapterTitle = mediaInfo?.metadata?.title || title;
const chapters = [ { key: "01", title: chapterTitle, overlayLabel: "1", tracks: [ { key: "01", title: chapterTitle, trackUrl: `yoto:#${transcodedAudio.transcodedSha256}`, duration: mediaInfo?.duration, fileSize: mediaInfo?.fileSize, channels: mediaInfo?.channels, format: mediaInfo?.format, type: "audio", overlayLabel: "1", display: { icon16x16: "yoto:#aUm9i3ex3qqAMYBv-i-O-pYMKuMJGICtR3Vhf289u2Q", }, }, ], display: { icon16x16: "yoto:#aUm9i3ex3qqAMYBv-i-O-pYMKuMJGICtR3Vhf289u2Q", }, }, ];
// Create the complete content object for a new card const content = { title: title, content: { chapters, }, metadata: { media: { duration: mediaInfo?.duration, fileSize: mediaInfo?.fileSize, readableFileSize: Math.round((mediaInfo?.fileSize / 1024 / 1024) * 10) / 10, }, }, };
console.log("Creating card with data:", content);
// Step 5: Create the new card const createCardResponse = await fetch("https://api.yotoplay.com/content", { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify(content), });
if (!createCardResponse.ok) { const errorText = await createCardResponse.text(); throw new Error(`Failed to create card: ${errorText}`); }
console.log("Progress: complete - 100%");
return await createCardResponse.json();};