如何让 Safari 12 处理来自 soundcloud 的音频?
Posted
技术标签:
【中文标题】如何让 Safari 12 处理来自 soundcloud 的音频?【英文标题】:How to get Safari 12 to process audio from soundcloud? 【发布时间】:2019-02-26 13:36:01 【问题描述】:在过去 5 个月的某个时候,在 Safari(ios 和 MacOS)中使用带有 WebAudio API 的 soundcloud 音频似乎已经坏了。它在 2018 年夏季工作。
我想知道是否有人找到了解决方法,或者我只是做错了什么。
这是一个播放来自 2 个来源之一的音频的示例。如果源是 soundcloud,它可以在 Chrome 和 Firefox 上运行,但在 Safari 上失败。如果源不是 soundcloud,它适用于所有 3 个浏览器。该示例不允许您实时切换,因此请检查 soundcloud 或不运行它。要尝试其他选项,请单击重新加载按钮。
检查网络标头没有任何问题。两个站点都在设置 CORS 标头,并且正如所指出的,它可以在 Firefox 和 Chrome 中使用
"use strict";
const log = console.log.bind(console);
const ctx = document.querySelector("canvas").getContext("2d");
ctx.fillText("click to start", 100, 75);
ctx.canvas.addEventListener('click', start);
document.querySelector('#reload').addEventListener('click', () =>
window.location.reload();
);
// Make a audio node
const audio = new Audio();
audio.loop = true;
audio.autoplay = true;
// have something ready to play when the user clicks to start
audio.src = getSilentMP3DataURL();
function objectToSearchString(obj)
const parts = Object.entries(obj).filter(v => v[1] !== undefined).map((keyValue) =>
return keyValue.map(encodeURIComponent).join('=');
);
return `?$parts.join('&')`;
// we need to ask soundcloud for a URL for each track as they are temporary
// and encoded by client id
class SoundCloudAPI
constructor(clientId)
this.clientId = clientId;
async getMediaURLForTrack(url, options)
options = JSON.parse(JSON.stringify(options));
Object.assign(options,
client_id: this.clientId,
format: 'json',
'_status_code_map[302]': 200,
);
let status;
let location = "https://api.soundcloud.com" + url + objectToSearchString(options);
let result;
let done = false;
while (!done)
log('fetch:', location);
const req = await fetch(location);
result = await req.json();
log('result:', JSON.stringify(result));
location = result.location;
status = result.status;
done = !(status && status.substr(0, 3) === "302" && location)
return result.stream_url + objectToSearchString(client_id: this.clientId);
class OtherSiteAPI
constructor()
async getMediaURLForTrack()
await waitSeconds(1); // to simulate that we can't set the audio.src immediately when doing soundcloud
return 'https://twgljs.org/examples/sounds/DOCTOR%20VOX%20-%20Level%20Up.mp3';
function waitSeconds(secs)
return new Promise((resolve) =>
setTimeout(resolve, secs * 1000);
);
function start()
ctx.canvas.removeEventListener('click', start);
ctx.canvas.addEventListener('click', pause);
const soundcloudElem = document.querySelector('#soundcloud');
soundcloudElem.disabled = true;
const useSoundCloud = soundcloudElem.checked;
const scAPI = useSoundCloud
? new SoundCloudAPI('91f71f725804f4915f4cc95f69fff503')
: new OtherSiteAPI();
let connected = false;
// make a Web Audio Context
const context = new (window.AudioContext || window.webkitAudioContext)();
const analyser = context.createAnalyser();
const gainNode = context.createGain();
analyser.connect(gainNode);
gainNode.connect(context.destination);
// Make a buffer to receive the audio data
const numPoints = analyser.frequencyBinCount;
const audioDataArray = new Uint8Array(numPoints);
function render()
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// get the current audio data
analyser.getByteFrequencyData(audioDataArray);
const width = ctx.canvas.width;
const height = ctx.canvas.height;
const size = 5;
// draw a point every size pixels
for (let x = 0; x < width; x += size)
// compute the audio data for this point
const ndx = x * numPoints / width | 0;
// get the audio data and make it go from 0 to 1
const audioValue = audioDataArray[ndx] / 255;
// draw a rect size by size big
const y = audioValue * height;
ctx.fillRect(x, y, size, size);
ctx.fillText('click to pause/play', 20, 20);
requestAnimationFrame(render);
requestAnimationFrame(render);
audio.play();
scAPI.getMediaURLForTrack('/resolve', url: 'https://soundcloud.com/chibi-tech/lolitazia-season')
.then((url) =>
// this line is only needed if the music you are trying to play is on a
// different server than the page trying to play it.
// It asks the server for permission to use the music. If the server says "no"
// then you will not be able to play the music
// Note if you are using music from the same domain
// **YOU MUST REMOVE THIS LINE** or your server must give permission.
log('set audio.src:', url);
audio.crossOrigin = "anonymous";
audio.src = url;
audio.load();
)
.catch((error) =>
console.error(error);
if (error.stack)
console.error(error.stack);
);
// call `handleCanplay` when it music can be played
audio.addEventListener('canplay', handleCanplay);
function handleCanplay()
// connect the audio element to the analyser node and the analyser node
// to the main Web Audio context
if (!connected)
log('connect media');
connected = true;
const source = context.createMediaElementSource(audio);
source.connect(analyser);
function pause()
if (audio.paused)
audio.play();
else
audio.pause();
function getSilentMP3DataURL()
return "data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV";
canvas border: 1px solid black; display: block;
<div>
<input id="soundcloud" checked type="checkbox">: Use SoundCloud
<button id="reload" type="button">reload page</button>
</div>
<canvas></canvas>
【问题讨论】:
注意:截至 2021 年 7 月,Soundcloud 更改了他们的 API。现在需要一个服务器来在 soundcloud API 和网页之间进行协商。有一个例子here 【参考方案1】:我在 Safari 上的 SoundCloud 中的 html AudioContext 中流式传输时遇到了同样的问题。问题似乎是从 SoundCloud API 重定向到实际音频文件。以下解决方法对我有用:
const response = await fetch(`$track.stream_url?client_id=$Helper.SOUNDCLOUD_CLIENT_ID`,
method: 'HEAD'
);
return response.url;
sn-p 对 API 资源执行 HTTP HEAD 请求,遵循重定向并返回重定向 URL,该 URL 可用于音频元素。我将它与声音分析仪一起使用来显示一些声音可视化。
【讨论】:
【参考方案2】:这听起来确实是一个奇怪的 Safari 错误,您可能希望让他们知道。
对于一种解决方法,您可以通过直接执行 Media -> Web Audio 来避免Media -> MediaElement -> MediaStream -> Web Audio 的漫长道路。
首先将您的媒体作为 ArrayBuffer 获取,然后从该媒体中解码音频数据并使用 AudioBufferSourceNode 播放它。
"use strict";
const log = console.log.bind(console);
const ctx = document.querySelector("canvas").getContext("2d");
ctx.fillText("click to start", 100, 75);
ctx.canvas.addEventListener('click', start);
function objectToSearchString(obj)
const parts = Object.entries(obj).filter(v => v[1] !== undefined).map((keyValue) =>
return keyValue.map(encodeURIComponent).join('=');
);
return `?$parts.join('&')`;
// we need to ask soundcloud for a URL for each track as they are temporary
// and encoded by client id
class SoundCloudAPI
constructor(clientId)
this.clientId = clientId;
async getMediaURLForTrack(url, options)
options = JSON.parse(JSON.stringify(options));
Object.assign(options,
client_id: this.clientId,
format: 'json',
'_status_code_map[302]': 200,
);
let status;
let location = "https://api.soundcloud.com" + url + objectToSearchString(options);
let result;
let done = false;
while (!done)
log('fetch:', location);
const req = await fetch(location);
result = await req.json();
log('result:', JSON.stringify(result));
location = result.location;
status = result.status;
done = !(status && status.substr(0, 3) === "302" && location)
return result.stream_url + objectToSearchString(client_id: this.clientId);
function start()
ctx.canvas.removeEventListener('click', start);
ctx.canvas.addEventListener('click', pause);
const scAPI = new SoundCloudAPI('91f71f725804f4915f4cc95f69fff503')
// make a Web Audio Context
const context = new (window.AudioContext || window.webkitAudioContext)();
const analyser = context.createAnalyser();
const gainNode = context.createGain();
analyser.connect(gainNode);
gainNode.connect(context.destination);
// Make a buffer to receive the audio data
const numPoints = analyser.frequencyBinCount;
const audioDataArray = new Uint8Array(numPoints);
function render()
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
analyser.getByteFrequencyData(audioDataArray);
const width = ctx.canvas.width;
const height = ctx.canvas.height;
const size = 5;
// draw a point every size pixels
for (let x = 0; x < width; x += size)
// compute the audio data for this point
const ndx = x * numPoints / width | 0;
// get the audio data and make it go from 0 to 1
const audioValue = audioDataArray[ndx] / 255;
// draw a rect size by size big
const y = audioValue * height;
ctx.fillRect(x, y, size, size);
ctx.fillText('click to pause/play', 20, 20);
requestAnimationFrame(render);
requestAnimationFrame(render);
scAPI.getMediaURLForTrack('/resolve', url: 'https://soundcloud.com/chibi-tech/lolitazia-season')
.then((url) => fetch(url)) // fetch our media
.then(r => r.arrayBuffer()) // as ArrayBuffer
// and decode it
.then(buf => context.decodeAudioData(buf))
.then(audioBuf =>
// now create an audiobuffer source node
const source = context.createBufferSource();
source.buffer = audioBuf;
source.loop = true;
source.connect(analyser)
source.start(0);
return source;
)
.catch((error) =>
console.error(error);
if (error.stack)
console.error(error.stack);
);
// For pause/play we will pause the entire context,
// we could also stop the buffer source node
// and start a new one with offset everytime if needed
function pause()
if (context.state === "suspended")
context.resume();
else
context.suspend();
canvas border: 1px solid black; display: block;
<!-- Safari doesn't support Promise syntax of decodeAudioData -->
<script src="https://cdn.jsdelivr.net/gh/mohayonao/promise-decode-audio-data@eb4b1322113b08614634559bc12e6a8163b9cf0c/build/promise-decode-audio-data.min.js"></script>
<canvas></canvas>
【讨论】:
谢谢。不幸的是,我将您的解决方案称为漫长的道路。我的需求是流媒体。让用户等待 1-10 分钟,而 4 到 120 分钟的音频下载有点违背了目的。当我发布这个时,我确实提交了一个错误。苹果用 rdar 链接标记了它,所以我猜他们知道但他们没有提供任何细节 @gman 在这种情况下,您是否检查了它与MSE 的行为?由于我们显然可以毫无问题地获取,并且 MediaElements 能够发出范围请求,我们没有理由不能,也许 Safari 不会遇到同样的错误。但这意味着您事先知道正确的编解码器,而我对 SoundCloud API 的了解不足以知道它们是否提供恒定格式/编解码器。 做了一个快速测试,虽然我很幸运 Safari 确实支持audio/mpeg
(与 FF 相反),但似乎他们的 MediaSourceElement 完全被破坏了,因为当元素的源指向时它会默默地失败一位女士...以上是关于如何让 Safari 12 处理来自 soundcloud 的音频?的主要内容,如果未能解决你的问题,请参考以下文章
iOS 12 Safari:有没有办法让基于 Web 的 QR 扫描工作?