标签: macos

  • iOS Safari 播放音频的技巧分享

    iOS Safari 播放音频的技巧分享

    开发 Web App 一直是个蛮尴尬的事情。一方面,Google 不断在推;PWA 等技术也越来越好;另一方面,当我们真的像用 Web 开发 App 的时候,总会在不同地方踩到各种坑,猝不及防。尤其是 iOS Safari,各种 bug 层出不穷,坑死个人。今天分享一下在 iOS Safari 播放音频的一些技巧,希望节省大家将来的时间。

    基础:使用 new Audio() 创建播放器

    播放声音看起来并不难,在桌面上实现也比较简单,直接 const audio = new Audio(音频地址); audio.play() 就好了。只要你的音频文件可以正常访问,那你应该很快就能听到声音。

    这个过程实际上创建了一个 HTMLAudioElement 的实例。所以,你也可以在页面上直接插入一个 <audio>,然后通过某种方式拿到它的引用,再使用它播放音频。

    除了初始化的时候传入音频地址,在任何时候改变音频地址,然后再 .play() 都可以,很简单。

    在用户点击时提前创建播放器

    但是在 iOS Safari 上,就有问题。因为手机端的限制更严格,只有用户主动操作(点击、敲击)时,新建的 <audio> 才能正常播放。如果你在其它时间创建 <audio> 并播放,一切看起来都是正常的,没有任何报错,网络也会正常产生请求,但就是不会发出声音。

    解决方案就是在用户产生点击、敲击的时候,就把 <audio> 创建好,然后在需要播放的时候让它播放声音。

    双播放器切换实现连续播放

    有时候我们需要循环连续播放一首歌,比如游戏背景音乐,或者打坐的时候播一些背景音。这个时候我们也有两个选择:

    1. 使用 <audio>
      • 好处:流式播放,速度会更快;有 loop 属性,更方便。
      • 坏处:如果是长音频,由于流式播放的原因,用过的数据可能被随时丢弃,所以新的一遍开始时可能会卡 0.N 秒,无法避免
    2. 使用 Web Audio API 等更高级的 API
      • 好处:音频在内存里,想怎么播怎么播,还可以加入各种效果
      • 坏处:第一次播需要完成加载,或者手动实现音频流,很麻烦

    我建议用第二个方法,使用起来比较简单。只要在开始的时候创建两个 <aduio> 对象,然后在第一个播放结束前 250ms 启动第二个即可。至于为什么是 250ms,因为按照规范,onTimeUpdate 的触发周期是最长不超过 250ms,所以这样可以比较好的切换播放器。

    以下是我在 Nuxt3 里使用的 composable,供大家参考:

    import type { AudioOptions } from '~/types';
    
    export function useAudio({
      src,
      loop,
      loopOverlap,
      autoplay,
    }: AudioOptions = {}) {
      const audio = shallowRef<HTMLAudioElement>();
      const backup = shallowRef<HTMLAudioElement>();
      const isStarting = ref<boolean>(false);
      const isPlaying = ref<boolean>(false);
      const playhead = ref<number>(0);
      const duration = ref<number>(0);
    
      async function playAudio(src: string, overlap: number = 0.25): Promise<void> {
        if (!audio.value) {
          initAudio(src);
        }
        if (audio.value) {
          audio.value.src = src;
          if (loop && backup.value) {
            backup.value.src = src;
          }
        }
        if (overlap !== undefined) {
          loopOverlap = overlap;
        }
        isStarting.value = true;
        await audio.value?.play();
      }
      function pauseAudio(): void {
        audio.value?.pause();
        backup.value?.pause();
      }
      function initAudio(src: string): void {
        audio.value = new Audio(src);
        if (src) {
          audio.value.preload = 'auto';
        }
        audio.value.autoplay = autoplay || false;
        if (loop) {
          backup.value = new Audio(src);
          backup.value.preload = 'auto';
          backup.value.autoplay = false;
          backup.value.addEventListener('ended', onEnded);
          backup.value.addEventListener('timeupdate', onTimeUpdate);
          backup.value.addEventListener('play', onPlay);
          backup.value.addEventListener('playing', onPlaying);
          backup.value.addEventListener('pause', onPause);
          backup.value.addEventListener('loadedmetadata', onMetadata);
        }
        audio.value.addEventListener('timeupdate', onTimeUpdate);
        audio.value.addEventListener('loadedmetadata', onMetadata);
        audio.value.addEventListener('ended', onEnded);
        audio.value.addEventListener('pause', onPause);
        audio.value.addEventListener('play', onPlay);
        audio.value.addEventListener('playing', onPlaying);
      }
      function destroy(): void {
        if (audio.value) {
          audio.value.removeEventListener('timeupdate', onTimeUpdate);
          audio.value.removeEventListener('loadedmetadata', onMetadata);
          audio.value.removeEventListener('ended', onEnded);
          audio.value.removeEventListener('pause', onPause);
          audio.value.removeEventListener('play', onPlay);
          audio.value.removeEventListener('playing', onPlaying);
          audio.value = undefined;
        }
        if (!backup.value) return;
        backup.value.removeEventListener('ended', onEnded);
        backup.value.removeEventListener('loadedmetadata', onMetadata);
        backup.value.removeEventListener('timeupdate', onTimeUpdate);
        backup.value.removeEventListener('play', onPlay);
        backup.value.removeEventListener('playing', onPlaying);
        backup.value.removeEventListener('pause', onPause);
        backup.value = undefined;
      }
    
      function onPlay(): void {
        isPlaying.value = true;
      }
      function onPlaying(): void {
        isStarting.value = false;
      }
      function onPause(): void {
       isPlaying.value = false;
     }
      function onEnded(event: Event): void {
        if (loop) {
          (event.target as HTMLAudioElement).currentTime = 0;
        } else {
          isPlaying.value = false;
        }
      }
      function onTimeUpdate(): void {
        playhead.value = audio.value?.currentTime || 0;
        if (!loop) return;
        if (!playhead.value || !duration.value || playhead.value < duration.value - loopOverlap) return;
    
        backup.value?.play();
        // swap audio and backup
        const temp = audio.value;
        audio.value = backup.value;
        backup.value = temp;
      }
      function onMetadata(event: Event): void {
        duration.value = (event.target as HTMLAudioElement).duration || 0;
      }
    
      if (src) {
        initAudio(src);
      }
    
      return {
        audio,
        isStarting,
        isPlaying,
        playhead,
    
        playAudio,
        pauseAudio,
        destroy,
      };
    }
    

    使用 macOS Safari 调试 iOS Safari

    Safari 烂归烂,但是有一点:iOS Safari 的 bug 大多可以在 macOS 上复现。所以大多数时候,我们可以直接在 macOS 完成调试。但是有时候,我们必须在 iOS 上进行调试,比如涉及到输入框影响屏幕高度、或者上面提到的播放问题。

    于是,使用 macOS Safari 调试 iOS Safari 也是必备技能,需要的同学请看我的这个视频:

    总结

    我有时候会怀疑,苹果是不是故意劣化 Safari,以避免被 Web App 抢走原生 App 的市场份额,不然 iOS Safari 的 bug 不应该这么多。

    如果大家对 Web App 开发感兴趣,有相关的问题,欢迎留言讨论。

  • 在新 Mac 上配置 Perl

    在新 Mac 上配置 Perl

    最新的 macOS Catalina 10.15.1 自带 Perl 5.18,但是不包含包管理工具 Cpanm,所以要手动安装。

    第一步,安装 xcode。

    第二步,安装 homebrew

    第三步,安装最新 Perl 5.30(没有 Perl 6……)

    brew install perl

    安装完成之后,需要验证一下当前使用的 perl 路径 perl -v 如果还是 5.18,那么检查一下引用 whereis perl,应该能看到 /usr/local/bin/perl/usr/bin/perl 前面,并且指向 /usr/local/Cellar/perl/{version}/bin/perl,那么就退出当前 Terminal,重新打开,应该就好了。

    第四步,安装 Cpanm

    brew install cpanm

    第五步,安装其它包

    cpan install Test::Base
    cpan install IPC::Run