在过去的5个月中,在Safari(iOS和MacOS)中使用带有WebAudio API的soundcloud音频似乎已经破裂。它正在2018年夏天工作。
我想知道是否有人找到了解决方法,或者我是否只是做错了什么。
这是一个从两个来源之一播放音频的示例。如果源是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>
这确实听起来像一个奇怪的Safari bug,你可能想让他们知道它。
对于解决方法,您可以直接通过媒体 - > Web音频来避免漫长的道路媒体 - > MediaElement - > MediaStream - > Web音频。
首先将您的媒体作为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>