React自定義Audio播放組件


還是直接上代碼

/**
 * 音頻播放組件
 */
import React, { Component } from 'react';
import { Slider, Toast } from 'antd-mobile';

import icon_play from '@/asset/image/panda_book/readpage/icon_play.png';
import icon_pause from '@/asset/image/panda_book/readpage/icon_pause.png';

import './AudioItem.less';

class AudioItem extends Component {

  static defaultProps = {
    src: '', // 音頻地址
  }

  state = {
    isCanPlay: false, // 判斷音頻是否加載完成
    playStatus: false, // 播放狀態, true 播放中, false 暫停中,
    duration: 0, // 音頻的時長
    currentDuration: 0, // 當前的播放時長 
  }

  audioItem = null; // 把dom暴露給外部使用
  audio = new Audio(); // 一個音頻對象
  timer = null; // 做一個滑條的防抖
  interval = null; // 定時查詢播放時的當前時間

  // 播放音頻
  play = () => {
    this.audio.play();
    this.interval = setInterval(() => {
      const time = Math.floor(this.audio.currentTime);
      if(time < this.state.duration) {
        this.setState({
          currentDuration: time
        });
      } else {
        // 播放結束后,直接重置播放時間,停止播放
        Toast.info('播放完畢');
        clearInterval(this.interval);
        this.audio.currentTime = 0;
        this.audio.pause();
        this.setState({
          currentDuration: 0,
          playStatus: false
        });
      }
    }, 1000);
  }

  // 暫停音頻
  pause = () => {
    this.audio.pause();
    if(this.interval) {
      clearInterval(this.interval);
    }
  }

  // 播放狀態切換
  handlePlayStatusChange = () => {
    const { playStatus, isCanPlay } = this.state;
    // 由於ios中不會預加載音頻資源,所以只好去掉加載狀態的判斷,如果有好的建議也可以提
    // if(!isCanPlay) {
    //   Toast.info('音頻還沒加載完呢~');
    //   return;
    // }
    if(!playStatus) {
      this.play();
    } else {
      this.pause();
    }
    this.setState({
      playStatus: !playStatus
    });
  };

  handleSilderChange = (value) => {
    if(this.timer) {
      clearTimeout(this.timer);
    }
    // 0.2s之內沒有改動就修改當前的時間,做一個播放的防抖
    this.timer = setTimeout(() => {
      this.pause();
      this.audio.currentTime = value;
      this.setState({
        currentDuration: value
      }, () => {
        if(this.state.playStatus) {
          this.play();
        }
      })
    }, 200);
  }

  // 根據秒數,返回對應的xx:xx的時間格式
  getDurationString = (number) => {
    let num = Number(number);
    if(isNaN(num) || num <= 0) {
      return '00:00';
    }
    if(num === Infinity) {
      return '00:00';
    }
    if(num < 60) {
      return `00:${num.toString().padStart(2, 0)}`;
    } else if(num < 3600) {
      const minute = Math.floor(num / 60);
      const second = num % 60;
      return `${minute.toString().padStart(2, 0)}:${second.toString().padStart(2, 0)}`
    } else {
      const hour = Math.floor(num / 3600);
      const minute = Math.floor((num - (hour * 3600)) / 60);
      const second = num - (hour * 3600) - (minute * 60);
      return `${hour.toString().padStart(2, 0)}:${minute.toString().padStart(2, 0)}:${second.toString().padStart(2, 0)}`;
    }
  }

  init = (props) => {
    const { src } = props || this.props;
    if(!src) {
      return;
    }
    this.audio.preload = 'automatic';
    this.audio.src = src;
    this.audio.load();
    // this.audio.src = 'http://www.yinpin.com/upload/yingxiaobusanlingfuwu0413017j.mp3';
    // 監聽音頻的時長是否獲取到了
    this.audio.ondurationchange = () => {
      const duration = Math.floor(this.audio.duration);
      this.setState({
        duration
      });
    }
    // 監聽音頻是否可以播放了
    this.audio.oncanplay = () => {
      const duration = Math.floor(this.audio.duration);
      this.setState({
        duration,
        isCanPlay: true
      });
    }
  };

  componentDidMount() {
    this.init();
  }

  componentWillReceiveProps(nextProps) {
    if(nextProps.src !== this.props.src) {
      this.init(nextProps);
    }
  }

  componentWillUnmount() {
    if(this.timer) {
      clearTimeout(this.timer);
    }
    if(this.interval) {
      clearInterval(this.interval);
    }
    if(this.audio) {
      this.audio.currentTime = 0;
      this.audio.pause();
    }
  }

  render() {
    const {
      playStatus,
      duration,
      currentDuration
    } = this.state;
    const btn_img = playStatus ? icon_pause : icon_play;
    const durationStr = this.getDurationString(duration);
    const currentDurationStr = this.getDurationString(currentDuration);
    return (
      <div className="AudioItem" ref={audioItem => this.audioItem = audioItem}>
        <div className="audio-item">
          {/* 播放按鈕 */}
          <div className="audio-item-btn" onClick={this.handlePlayStatusChange}>
            <img src={btn_img} alt="icon" />
          </div>
          <div className="audio-item-content">
            <div className="audio-item-top">
              {/* 播放中的動畫 */}
              <div className={`audio-item-bars ${playStatus ? 'animate' : ''}`}>
                <div className="audio-item-bar"></div>
                <div className="audio-item-bar"></div>
                <div className="audio-item-bar"></div>
                <div className="audio-item-bar"></div>
                <div className="audio-item-bar"></div>
              </div>
              {/* 播放的時長 */}
              <div className="audio-item-range">
                <div className="audio-item-current">{currentDurationStr}</div>
                <div className="audio-item-duration">{durationStr}</div>
              </div>
            </div>
            <div className="audio-item-bottom">
              <Slider
                trackStyle={{
                  height: '0.13rem',
                  backgroundColor: '#10C0DC',
                  borderRadius: '0.07rem 0 0 0.07rem'
                }}
                railStyle={{
                  height: '0.13rem',
                  backgroundColor: '#DEEAEC',
                  borderRadius: '0 0.07rem 0.07rem 0'
                }}
                handleStyle={{
                  width: '0.32rem',
                  height: '0.32rem',
                  border: 'none',
                  marginTop: '-0.1rem',
                  borderRadius: '50%',
                  backgroundColor: '#FFFFFF',
                  boxShadow: '0.08rem 0.08rem 0.21rem rgba(140, 181, 187, 0.3), -0.08rem -0.08rem 0.21rem rgba(140, 181, 187, 0.3)'
                }}
                style={{ marginLeft: '0.34rem', marginRight: 0 }}
                value={currentDuration}
                min={0}
                max={duration}
                onChange={this.handleSilderChange}
              />
            </div>
          </div>
        </div>
      </div>
    );
  }
}

export default AudioItem;
.AudioItem {
  display: flex;
  justify-content: center;
  width: 100%;

  .audio-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    width: 9.09rem;
    height: 1.87rem;
    padding: 0 0.4rem;
    background-color: #F6F8FA;
    border-radius: 0.2rem;

    &-btn {
      width: 0.96rem;
      height: 0.96rem;
      img {
        width: 100%;
        height: 100%;
      }
    }

    &-content {
      width: calc(100% - 1.32rem);
    }

    &-top,
    &-range {
      display: flex;
      align-items: center;
      justify-content: space-between;
    }

    &-top {
      margin-bottom: 0.26rem;
    }

    &-bars {
      display: flex;
      align-items: center;
      width: 0.48rem;
      height: 0.48rem;
      overflow: hidden;

      &.animate {
        .audio-item-bar {
          &:nth-child(1),
          &:nth-child(5) {
            animation: playAudio1 0.8s infinite ease-in;
          }

          &:nth-child(2),
          &:nth-child(4) {
            animation: playAudio2 0.8s infinite ease-in-out;
          }

          &:nth-child(3) {
            animation: playAudio3 0.8s infinite ease-out;
          }
        }
        @keyframes playAudio1 {
          0%, 100% {
            height: 0.2rem;
          }
          50% {
            height: 0.48rem;
          }
        }
        @keyframes playAudio2 {
          0%, 50%, 100% {
            height: 0.3rem;
          }
          25% {
            height: 0.48rem;
          }
          75% {
            height: 0.2rem;
          }
        }
        @keyframes playAudio3 {
          0%, 100% {
            height: 0.48rem;
          }
          50% {
            height: 0.2rem;
          }
        }
      }
    }

    &-range {
      width: calc(100% - 0.88rem);
    }

    &-bottom {
      width: 100%;
      height: 0.13rem;
    }

    &-bar {
      flex-shrink: 0;
      flex-grow: 0;
      width: 0.4rem;
      height: 0.48rem;
      transform-origin: left center;
      transform: scaleX(0.1);
      border-radius: 0.1rem;
      background-color: #10C0DC;

      &:nth-child(1),
      &:nth-child(5) {
        height: 0.2rem;
      }

      &:nth-child(2),
      &:nth-child(4) {
        height: 0.3rem;
      }

      &:not(:first-child) {
        margin-left: -0.3rem;
      }
    }
  }
}

遇到的問題

  • 有時候音頻無法設置currentTime屬性,這個是由於服務端的響應頭中的cache-control有問題,改一下響應頭就可以了。
  • 針對有些音頻無法在audio這邊獲取到duration的,比如ios端好像在播放之前拿不到duration,估計是要等播放之后才能去獲取,還沒嘗試,主要是沒ios設備,不好測試。這種情況建議是后端把音頻的duration直接返回過來。
  • 滑動改變進度的時候,由於用的時候阿里的UI組件Slider,實際效果不是很好,有時間的可以自己寫一個。
  • 針對於需要播放的音頻如果過大的,最好做好預加載資源。
  • 其實還有很多地方沒有考慮到,不過如果僅僅只是簡單播放就足夠了,引用的第三方組件實在是不喜歡改別人的樣式,自己寫的話,可定制化更高。

樣式預覽


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM