H5 audio 音频拖动(文件流不返回总时长的情况)

音频文件没有返回duration

即const duration = this.$refs.audio.duration;duration为infinity或者NaN

要实现拖动有两种方法:

第一种

如果要返回正常数据的duration需要后端做一下两步:

1:audio标签获取不到总时长的缘故是当后台写回数据流,响应头没有Content-Length时,无法获取到总时长,只有加上该响应头,audio标签才能正确的获取总时长。

    @RequestMapping("audio")
    public void audio(HttpServletResponse response) throws IOException {
        File f = new ClassPathResource("a.wav").getFile();
        FileInputStream fis = new FileInputStream(f);
        // 无所谓,application/octet-stream也可以,不写也可以
        response.setHeader("Content-Type", "audio/wav");
        // 关键所在,如果后台直接读取文件流写回客户端,无Content-Length,则audio标签无法获取总时长
        response.setHeader("Content-Length", f.length() + "");
        byte[] buf = new byte[4096];
        int len;
        while ((len = fis.read(buf)) != -1) {
            response.getOutputStream().write(buf, 0, len);
        }
        response.getOutputStream().flush();
    }

2:拖动条不能拖动问题:后端需要继续增加响应头 Content-Range, Accept-Ranges**

    @GetMapping("audio/{fileName}")
    public void audio(HttpServletResponse response, HttpServletRequest request, @PathVariable("fileName") String fileName) throws IOException {
        File f = new ClassPathResource(fileName).getFile();
        FileInputStream fis = new FileInputStream(f);
 
        // audio / video 标签拖动时发送的请求, 第一次:bytes=0-, 后续:bytes=startPos-endPos
        String rangeString = request.getHeader("Range");
        // range 值
        long rangeStart = Long.parseLong(rangeString.substring(rangeString.indexOf("=") + 1, rangeString.indexOf("-")));
        // range范围是从0个字节开始,所以结束位置下标是文件长度-1
        long rangeEnd = f.length() - 1;
        // 解析请求Range头信息,bytes=xxx-xxx, (存在0-0,-1这种情况,暂不考虑)
        if (rangeString.indexOf("-") < rangeString.length() - 1) {
            rangeEnd = Long.parseLong(rangeString.substring(rangeString.indexOf("-") + 1));
        }
 
        // chrome浏览器第一次发送请求,range开始为0
        if (rangeStart == 0) {
            // 没有这个header,audio没有总时长,第一次请求返回总长度
            response.setHeader("Content-Length", f.length() + "");
        } else {
            // 配合content-range头使用,后续Content-Length代表分段大小
            response.setStatus(HttpStatus.PARTIAL_CONTENT.value());
            response.setHeader("Content-Length", (rangeEnd - rangeStart + 1) + "");
        }
 
        // 不太重要
        response.setHeader("Content-Type", fileName.endsWith("wav") ? "audio/wav" : "audio/mp3");
 
        // range范围  start-end/total, 这个头可以不要,但是如果不要(content-length必须为文件总大小),每次拖拽,浏览器请求的文件都是全量大小的文件, 浪费带宽
        response.setHeader("Content-Range", "bytes " + rangeStart + "-" + rangeEnd + "/" + f.length());
 
        // 告知video / audio标签可以接受范围单位,这个头必须传递
        response.setHeader("Accept-Ranges", "bytes");
 
        // 不重要
        response.setHeader("content-disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
 
        // skip bytes
        if (rangeStart > 0) {
            long skipped = fis.skip(rangeStart);
            System.out.println(fileName + ", skipped: " + skipped + ", total: " + f.length());
        }
 
        byte[] buf = new byte[4096];
        int len;
        long total = 0L;
        while ((len = fis.read(buf)) != -1) {
            total += len;
            response.getOutputStream().write(buf, 0, len);
        }
        System.out.println(fileName + ", write: " + total + ", range: " + (rangeEnd - rangeStart));
        response.getOutputStream().flush();
    }

然后前端正常用H5 audito

第二种
通过 v-if 多次渲染dom

实现了播放,暂停,快进,快退,音量控制

之前业务的前端代码:

<template>
  <div class="yx-audio-container">
    <div v-loading="loading" class="main">
      <div class="flex-between box">
        <div class="left duration" @mousedown="isDraing=true" @mouseup="isDraing=false">
          <input ref="range" class="cursor-pointer" type="range" min="0" max="360" value="0" @input="onChange">
          <!-- @change="onChange" -->
        </div>
        <div class="right time">{{ formatCurrentTime }} / {{ formatTotalTime }}</div>
      </div>
      <div class="flex-between box">
        <div class="left">
          <span class="icon icon-setp" @click="onChangeSelf({type:'left'})">
            <i class="el-icon-d-arrow-left"></i>
          </span>
          <span v-if="isPlay == false" class="icon" @click="play">
            <i class="el-icon-video-play"></i>
          </span>
          <span v-if="isPlay == true" class="icon" @click="pause">
            <i class="el-icon-video-pause"></i>
          </span>
          <span class="icon icon-setp" @click="onChangeSelf({type:'right'})">
            <i class="el-icon-d-arrow-right"></i>
          </span>
        </div>
        <div class="right">
          <div class="words flex-between">
            <!-- <div class="name">{{ audioName==null ? "未知": audioName }}</div> -->
            <img class="volumeicon icon" :src="volumeicon" >
            <input ref="rangeVolume" class="cursor-pointer" type="range" min="0" max="100" value="100" @input="onChangeVolume"></input>
          </div>
        </div>
      </div>
    </div>
    <!-- v-if="show"  -->
    <audio v-if="show" ref="audio" style="display: none" :src="url" controls :preload="preload" :autoplay="autoplay" @timeupdate="timeupdate" @canplay="loadingFinish" @ended="ended"></audio>
  </div>
</template>

<script>
import volumeicon from '@/assets/icons/volume.png'
export default {
  name: 'AudioPlayer',
  components: {},
  props: {
    url: {
      type: String,
      default: '../未知'
    },
    // 播放总时间--秒 这个项目音频总获取不到时长
    duration: {
      type: Number,
      default: 0
    },
    autoplay: { // 声明该属性,音频会尽快自动播放,不会等待整个音频文件下载完成
      type: Boolean,
      default: false
    },
    preload: {
      type: String,
      default: 'auto'
    }
  },
  data() {
    return {
      loading: false,
      volumeicon,
      isDraing: false,
      isPlay: false, // 控制icon切换
      show: true, // 多次渲染audtio fix音频没有时长不能拖动问题
      timeout: null,
      volumeValue: 100,
      endedTime: 0, // 播放结束后获取时间和 和控制组件多次渲染
      totalTime: 0, // 播放总时间--秒
      currentTime: 0 // 当前播放时间--秒
    }
  },
  computed: {
    formatTotalTime() {
      return this.formatTime(this.totalTime)
    },
    formatCurrentTime() {
      return this.formatTime(this.currentTime)
    },
    // 音频名称
    audioName() {
      return this.getFilename(this.url)
    }
  },
  watch: {
    endedTime(nv, ov) {
      if (!ov && nv) {
        this.show = false
        this.timeout && clearTimeout(this.timeout)
        console.log(nv, 'endedTime')
        this.timeout = setTimeout(() => { this.show = true }, 2000)
      }
    }
  },
  mounted() {
    console.log(this.duration, 'durationmounted')
    this.initData()
  },
  methods: {
    initData(type) {
      // this.loading = true,
      // this.show = false
      // this.timeout && clearTimeout(this.timeout)
      // this.timeout = setTimeout(() => { this.show = true }, 1000)
      this.endedTime = 0
      this.isPlay = false

      this.$nextTick(() => {
        this.$refs.range.value = 0
        this.$refs.rangeVolume.value = this.volumeValue
        if (type && type === 'pause') {
          this.pause()
        }
        if (type && type === 'play') {
          this.play()
        }
      })
    },
    // 控制音乐播放
    play() {
      const audio = this.$refs.audio
      audio.play()
      this.isPlay = true
    },
    // 控制音乐暂停
    pause() {
      const audio = this.$refs.audio
      audio.pause()
      this.isPlay = false
    },
    // 音乐缓存完毕,获取时间
    loadingFinish() {
      this.loading = false

      const duration = this.$refs.audio.duration
      // 获取逻辑顺序文件本身总时长,结束后获取总时长,父级传参时长,当前播放时长,默认20s
      const totalTime = Number.isFinite(duration) ? duration : this.endedTime ? this.endedTime : this.duration ? this.duration : this.currentTime || 20
      console.log(duration, 'duration')
      console.log(this.endedTime, 'this.endedTime')
      console.log(this.duration, 'this.duration')
      console.log(this.currentTime, 'this.currentTime')

      this.totalTime = totalTime
      this.endedTime = 0

      // this.play()
      // this.initData()
    },
    // range--拖动进度条得到的回调
    onChange() {
      this.$nextTick(() => {
        const value = this.$refs.range.value
        const persentage = ((value / 360) * 100).toFixed(1) + '%'
        this.$refs.range.style.backgroundSize = `${persentage} 100%`
        // 控制音频播放
        const timeToGo = (value / 360) * this.totalTime
        const audio = this.$refs.audio
        console.log(timeToGo, 'timeToGo')
        audio.currentTime = timeToGo
        // this.currentTime = timeToGo
      })
    },
    // 快进 快退
    onChangeSelf(data) {
      const {type, step = 10} = data
      this.$nextTick(() => {
        const audio = this.$refs.audio
        let currentTime = audio.currentTime // 当前播放时间
        if (type && type === 'right') {
          currentTime = currentTime + step
          if (currentTime >= this.totalTime) {
            currentTime = this.totalTime
          }
        }
        if (type && type === 'left') {
          currentTime = currentTime - step
          if (currentTime <= 0) {
            currentTime = 0
          }
        }
        this.currentTime = currentTime

        // 控制音频播放
        console.log(currentTime, 'timeToGo')
        audio.currentTime = currentTime

        // 改变进度条的值
        const range = this.$refs.range
        range.value = ((this.currentTime / this.totalTime) * 360).toFixed(1)
        // 进度条的值改变的时候,颜色也跟着变化
        const persentage = ((this.currentTime / this.totalTime) * 100).toFixed(1) + '%'
        this.$refs.range.style.backgroundSize = `${persentage} 100%`
      })
    },
    // audio--进度变化的时候的回调--改变文字
    timeupdate() {
      // this.isPlay = true
      console.log('timeupdate')
      if (!this.isDraing) {
        const audio = this.$refs.audio
        const currentTime = audio.currentTime // 当前播放时间
        this.currentTime = currentTime

        // 纠正总时间
        if (this.currentTime && this.currentTime > this.totalTime) {
          this.totalTime = this.currentTime
        }
        // 改变进度条的值
        const range = this.$refs.range
        range.value = ((this.currentTime / this.totalTime) * 360).toFixed(1)
        // 进度条的值改变的时候,颜色也跟着变化
        const persentage = ((this.currentTime / this.totalTime) * 100).toFixed(1) + '%'
        this.$refs.range.style.backgroundSize = `${persentage} 100%`

        // var target = e.target
        //     console.log('当前时间:', target.currentTime, '总时长:', target.duration);
        //     if (!Number.isFinite(target.duration)) {
        //         target.currentTime = Number.MAX_SAFE_INTEGER;
        //         target.currentTime = 0;
        //     } else {
        //         // 在这个地方做你和更新时间进度有关的事情,
        //         // 比如更新自定义播放进度条样式等
        //     }
      }
    },
    // 音频结束
    ended() {
      const duration = this.$refs.audio.duration
      this.endedTime = Number.isFinite(duration) && duration || 0
      this.isPlay = false
    },
    // 改变声音
    onChangeVolume() {
      const audio = this.$refs.audio
      const rangeVolume = this.$refs.rangeVolume
      // audio.volume = rangeVolume.value;
      audio.volume = (rangeVolume.value / 100) * 1
      this.volumeValue = rangeVolume.value
      console.log(audio.volume, ' audio.volume ')
      // const str3 = value.target.value + '% 100%';
      // rangeVolume.current.style.backgroundSize = `${persentage} 100%`
    },
    // 辅助函数,将秒变成分秒的形式--用在计算属性中
    formatTime(value) {
      let second = 0
      let minute = 0
      minute = parseInt(value / 60)
      second = parseInt(value % 60)
      // 补0
      minute = minute < 10 ? '0' + minute : minute
      second = second < 10 ? '0' + second : second
      return minute + ':' + second
    },
    // 通过url获取filename
    getFilename(url) {
      const arr = url.split('/')
      return arr[arr.length - 1]
    }
  }
}
</script>

<style lang="scss" scoped>
$color-audio: #10a9ff; // 页面背景色

.main {
  padding: 8px 16px;
  // width: 100%;
  background: #f5f6f8;
  border-radius: 2px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.box {
  height: 40px;
  width: 100%;
}
.left {
  margin-right: 10px;
  width: 70%;
  display: flex;
  align-items: center;
}
.right {
  flex: 1;
}
// 播放暂停按钮
.icon {
  margin-right: 20px;
  color: $color-audio;
  i {
    font-size: 40px;
  }
}
.icon-setp {
  i {
    font-size: 24px;
  }
}
.icon:hover {
  cursor: pointer;
}
.play-icon {
  position: relative;
  left: 2px;
}
.flex-between {
  display: flex;
  justify-content: space-between;
  align-content: center;
  align-items: center;
}
.words {
  margin-bottom: -1px;
}
.name {
  font-size: 14px;
  color: #333333;
  line-height: 14px;
}
.time {
  font-size: 14px;
  color: #666666;
  // line-height: 14px;
  // margin-top: 6px;
}
.volumeicon {
  width:24px;
  height:24px;
  margin-right: 6px;
  margin-left: 36px;
}
// 控件
input[type='range'] {
  outline: none;
  -webkit-appearance: none; /*清除系统默认样式*/
  width: 100% !important;
  background: -webkit-linear-gradient($color-audio, $color-audio) no-repeat, #dddddd; /*背景颜色,俩个颜色分别对应上下*/
  background-size: 0% 100%; /*设置左右宽度比例,这里可以设置为拖动条属性*/
  height: 2px; /*横条的高度,细的真的比较好看嗯*/
}
/*拖动块的样式*/
input[type='range']::-webkit-slider-thumb {
  -webkit-appearance: none; /*清除系统默认样式*/
  height: 10px; /*拖动块高度*/
  width: 4px; /*拖动块宽度*/
  background: $color-audio; /*拖动块背景*/
}
</style>

据说可以这样,没试过:

image.png

参考:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/audio

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容