【问题标题】:How to get Safari 12 to process audio from soundcloud?如何让 Safari 12 处理来自 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

标签: ios macos safari soundcloud


【解决方案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 完全被破坏了,因为当元素的源指向时它会默默地失败一位女士...
    猜你喜欢
    • 1970-01-01
    • 2018-04-19
    • 2012-12-07
    • 1970-01-01
    • 2012-05-23
    • 1970-01-01
    • 2010-11-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多