上一篇文章用 Streamlit 写了一个录音按钮的组件,实现了按下去时开始录音、放开结束录音的功能。但是只支持桌面端网页用鼠标点击,这次对齐进行扩展,使其能够实现在手机端按下录音的功能。

Touch 事件

在桌面端监听的是鼠标的 mousedown/mouseup 事件,但是在移动端则是手指触屏屏幕的事件 touchstart/touchend,因此需要修改按钮的监听事件。另外之前采用样式中 :hover 来实现按下时按钮颜色的变化,现在使用一个变量来控制

<template>
    <div class="button-container">
      <button 
        @touchstart="startRecording"
        @touchcancel="stopRecording"
        @touchend="stopRecording"
        @mousedown="startRecording"
        @mouseup="stopRecording"
        @mouseleave="stopRecording"
        class="red-round-button" :style="{ backgroundColor: buttonColor }" ></button>
    </div>
    <audio ref="audioPlayer" controls class="audio-player"></audio>
</template>


<script setup>
...
const buttonColor = ref('red');
const dynamicStyles = computed(() => {
  return {
    width: props.args.width,
    height: props.args.height,
    backgroundColor: buttonColor
  };
});
...
</script>

另外,移动端长按按钮的话会唤出 Text Selection,需要取消在监听时间时取消掉,详见 Prevent text selection on tap and hold on iOS 13 / Mobile Safari

function startRecording(event) {
  event.preventDefault();
  ...
}
function stopRecording(event) {
  event.preventDefault();
  ...
}

录音格式兼容

由于 iOS 的浏览器录制的音频格式与桌面端浏览器不一样(参考 MediaRecorder: isTypeSupported() static method),需要在代码中先判断平台,再根据平台决定录音格式,其中桌面 Chrome 格式为 webm/opus,iOS Safari 为 mp4/aac,然后要将对应格式传回组件返回值

const isIOS = ref(false);

function checkPlatform() {
  if (navigator.userAgentData) {
    console.log(navigator.userAgentData)
    // Use userAgentData for modern browsers
    navigator.userAgentData.getHighEntropyValues(["platform"])
      .then(ua => {
        if (/iPhone|iPad|iPod/.test(ua.platform)) {
          isIOS.value = true;
        }
      });
  } else {
    // Fallback for browsers that do not support userAgentData
    const userAgent = navigator.userAgent;

    console.log(navigator.userAgent)
    if (/iPhone|iPad|iPod/.test(userAgent)) {
      isIOS.value = true;
    }
  }
}

// 在录音时根据平台选择格式
function stopRecording(event) {
  event.preventDefault();
  if (mediaRecorder.value && mediaRecorder.value.state !== 'inactive') {
    mediaRecorder.value.stop();
    mediaRecorder.value.onstop = async function() {
      let format = 'webm';
      if (isIOS.value) {
        format = 'mp4'
      }
      const audioBlob = new Blob(audioChunks.value, { type: 'audio/' + format });
      const reader = new FileReader();
      reader.onload = function(event) {
        if (event.target && event.target.result) {
          const dataUrl = event.target.result;
          const base64 = dataUrl.split(',')[1]; // Extract the base64 part
          Streamlit.setComponentValue({data: base64, format: format}); // 将数据与格式回传
        } else {
          console.log('Failed to read Blob as base64');
        }
      };
      reader.readAsDataURL(audioBlob);
    };
  }
}

onMounted(() => {
  // 先判断平台
  checkPlatform();
});

python 代码也要做对应修改

def record_button(size, key=None):
    component_value = _component_func(size=size, key=key, default=None)
    if component_value:
        component_value['data'] = base64.b64decode(component_value['data'])
    return component_value

audio = record_button(size='20px')
if audio:
    audio_data, _format = audio['data'], audio['format']
    print(f'len: {len(audio_data)}')
    st.audio(audio_data)
    buffer = io.BytesIO(audio_data)
    try:
        if _format == 'mp4':
            audio = AudioSegment.from_file(buffer, codec="aac")
        else:
            audio = AudioSegment.from_file(buffer, codec="opus")
    
        audio = audio.set_frame_rate(16000).export(format="mp3").read()
    
        query = flash_recognition(audio)
        st.markdown(f'you said: {query}')
        
        reply = completion(query)
        st.markdown(f'reply: {reply}')
    
    except Exception as e:
        st.error(e)

调试

手机的页面没有 F12 开发者工具不方便调试页面,好在 Apple 的 Mac 上的 Safari 可以打开手机 Safari 页面的调试工具

  1. 手机打开设置 -> Safari -> 高级 -> 网页检查器
  2. 打开 Mac Safari -> Settings -> Advanced -> Show features for web developers

具体步骤参考 Develop menu