Как заставить Safari 12 обрабатывать звук из soundcloud? - PullRequest
0 голосов
/ 26 февраля 2019

Иногда за последние 5 месяцев использование звука из звукового облака с API WebAudio в Safari (как для iOS, так и для MacOS), похоже, сломалось.Это работало летом 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>

Ответы [ 2 ]

0 голосов
/ 28 июля 2019

У меня та же проблема с потоковой передачей в HTML AudioContext из SoundCloud в Safari.Похоже, проблема заключается в перенаправлении из SoundCloud API в реальный аудиофайл.Следующий обходной путь работает для меня:

const url = await fetch(`${track.stream_url}?client_id=${Helper.SOUNDCLOUD_CLIENT_ID}`, {
  method: 'HEAD'
})
.then((response) => {
  return response.url;
});

Фрагмент выполняет HTTP HEAD-запрос к ресурсу API, следует за перенаправлением и возвращает URL-адрес перенаправления, который можно использовать для элемента Audio.Я использую его со звуковым анализатором для отображения некоторой визуализации звука.

0 голосов
/ 22 апреля 2019

Это действительно звучит как странная ошибка Safari, и вы можете сообщить им об этом.

Для обходного пути вы можете просто избежать долгого пути Media -> MediaElement -> MediaStream-> Веб-аудио здесь, выполнив Медиа -> Веб-аудио .

Сначала извлеките носитель как 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>
...