如何让 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 扫描工作?

如何使用 css 编写模式处理 safari ios 设备?

来自 Safari 扩展的注入脚本中的 AJAX

Safari 中的 UIWebView 打开链接不起作用?

Pormetheus

来自蓝牙键盘的 IOS7 上 Safari 中的 onkeyup 事件