Skip to content

uni-app 媒体组件指南

本指南详细介绍uni-app的媒体组件,包括图片、音频、视频、相机等多媒体功能组件的使用方法。

目录


image 图片组件

image 组件用于展示图片,支持 JPG、PNG、SVG、WEBP、GIF 等格式。

主要属性

属性名类型默认值说明
srcString-图片资源地址
modeStringscaleToFill图片裁剪、缩放的模式
lazy-loadBooleanfalse图片懒加载
fade-showBooleantrue图片显示动画效果
webpBooleanfalse是否解析webP格式
show-menu-by-longpressBooleanfalse长按显示识别菜单

mode 属性值

说明
scaleToFill不保持纵横比缩放图片,使图片完全适应
aspectFit保持纵横比缩放,长边完全显示
aspectFill保持纵横比缩放,短边完全显示
widthFix宽度不变,高度自动变化
heightFix高度不变,宽度自动变化
top不缩放,只显示顶部区域
bottom不缩放,只显示底部区域
center不缩放,只显示中间区域
left不缩放,只显示左边区域
right不缩放,只显示右边区域

使用示例

基础用法

vue
<template>
  <view class="container">
    <!-- 基本图片显示 -->
    <image 
      src="/static/logo.png" 
      mode="aspectFit"
      class="logo"
    />
    
    <!-- 网络图片 -->
    <image 
      src="https://example.com/image.jpg" 
      mode="widthFix"
      lazy-load
      @load="onImageLoad"
      @error="onImageError"
    />
  </view>
</template>

<script>
export default {
  methods: {
    onImageLoad(e) {
      console.log('图片加载完成', e.detail)
    },
    onImageError(e) {
      console.error('图片加载失败', e.detail.errMsg)
    }
  }
}
</script>

<style>
.container {
  padding: 20px;
}
.logo {
  width: 100px;
  height: 100px;
  border-radius: 8px;
}
</style>

不同缩放模式对比

vue
<template>
  <view class="container">
    <view class="mode-list">
      <view 
        class="mode-item" 
        v-for="mode in modes" 
        :key="mode"
      >
        <image 
          :src="imageUrl" 
          :mode="mode" 
          class="demo-image"
        />
        <text class="mode-name">{{ mode }}</text>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      imageUrl: 'https://example.com/demo.jpg',
      modes: [
        'scaleToFill', 'aspectFit', 'aspectFill', 
        'widthFix', 'heightFix', 'center'
      ]
    }
  }
}
</script>

<style>
.mode-list {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.mode-item {
  width: calc(50% - 5px);
  text-align: center;
}
.demo-image {
  width: 100%;
  height: 120px;
  border: 1px solid #eee;
  border-radius: 4px;
}
.mode-name {
  display: block;
  font-size: 12px;
  color: #666;
  margin-top: 5px;
}
</style>

video 视频组件

video 组件用于播放视频,支持多种视频格式和丰富的播放控制功能。

主要属性

属性名类型默认值说明
srcString-视频资源地址
autoplayBooleanfalse是否自动播放
loopBooleanfalse是否循环播放
mutedBooleanfalse是否静音播放
controlsBooleantrue是否显示默认播放控件
posterString-视频封面图片地址
initial-timeNumber0指定视频初始播放位置(秒)
durationNumber-指定视频时长(秒)
object-fitStringcontain视频填充模式
show-fullscreen-btnBooleantrue是否显示全屏按钮
show-play-btnBooleantrue是否显示播放按钮
show-center-play-btnBooleantrue是否显示视频中间的播放按钮
enable-progress-gestureBooleantrue是否开启控制进度的手势
danmu-listArray-弹幕列表
danmu-btnBooleanfalse是否显示弹幕按钮
enable-danmuBooleanfalse是否展示弹幕

使用示例

基础视频播放

vue
<template>
  <view class="container">
    <video
      id="myVideo"
      src="https://example.com/video.mp4"
      controls
      poster="https://example.com/poster.jpg"
      object-fit="cover"
      @play="onVideoPlay"
      @pause="onVideoPause"
      @ended="onVideoEnded"
      @timeupdate="onTimeUpdate"
      @error="onVideoError"
      class="video-player"
    />
    
    <view class="video-controls">
      <button @click="playVideo">播放</button>
      <button @click="pauseVideo">暂停</button>
      <button @click="seekTo30">跳转到30秒</button>
      <button @click="toggleMute">
        {{ isMuted ? '取消静音' : '静音' }}
      </button>
    </view>
    
    <view class="video-info">
      <text>当前时间: {{ currentTime }}s</text>
      <text>总时长: {{ totalDuration }}s</text>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      videoContext: null,
      isMuted: false,
      currentTime: 0,
      totalDuration: 0
    }
  },
  onReady() {
    this.videoContext = uni.createVideoContext('myVideo', this)
  },
  methods: {
    onVideoPlay() {
      console.log('视频开始播放')
    },
    onVideoPause() {
      console.log('视频暂停')
    },
    onVideoEnded() {
      console.log('视频播放结束')
    },
    onTimeUpdate(e) {
      this.currentTime = Math.floor(e.detail.currentTime)
      this.totalDuration = Math.floor(e.detail.duration)
    },
    onVideoError(e) {
      console.error('视频播放错误', e.detail)
    },
    playVideo() {
      this.videoContext.play()
    },
    pauseVideo() {
      this.videoContext.pause()
    },
    seekTo30() {
      this.videoContext.seek(30)
    },
    toggleMute() {
      this.isMuted = !this.isMuted
      // 注意:需要重新设置video组件的muted属性
    }
  }
}
</script>

<style>
.container {
  padding: 20px;
}
.video-player {
  width: 100%;
  height: 200px;
  border-radius: 8px;
}
.video-controls {
  display: flex;
  gap: 10px;
  margin: 15px 0;
  flex-wrap: wrap;
}
.video-controls button {
  flex: 1;
  min-width: 80px;
  padding: 8px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
}
.video-info {
  display: flex;
  justify-content: space-between;
  font-size: 14px;
  color: #666;
}
</style>

带弹幕的视频播放

vue
<template>
  <view class="container">
    <video
      id="danmuVideo"
      src="https://example.com/video.mp4"
      controls
      danmu-btn
      enable-danmu
      :danmu-list="danmuList"
      class="video-player"
    />
    
    <view class="danmu-input">
      <input 
        v-model="danmuText" 
        placeholder="输入弹幕内容"
        class="danmu-input-field"
      />
      <button @click="sendDanmu" class="send-btn">发送</button>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      videoContext: null,
      danmuText: '',
      danmuList: [
        { text: '第1秒弹幕', color: '#ff0000', time: 1 },
        { text: '第3秒弹幕', color: '#00ff00', time: 3 },
        { text: '第5秒弹幕', color: '#0000ff', time: 5 }
      ]
    }
  },
  onReady() {
    this.videoContext = uni.createVideoContext('danmuVideo', this)
  },
  methods: {
    sendDanmu() {
      if (!this.danmuText.trim()) return
      
      this.videoContext.sendDanmu({
        text: this.danmuText,
        color: this.getRandomColor()
      })
      
      this.danmuText = ''
    },
    getRandomColor() {
      const colors = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff']
      return colors[Math.floor(Math.random() * colors.length)]
    }
  }
}
</script>

<style>
.danmu-input {
  display: flex;
  gap: 10px;
  margin-top: 15px;
}
.danmu-input-field {
  flex: 1;
  height: 40px;
  padding: 0 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
}
.send-btn {
  width: 80px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
}
</style>

audio 音频组件

audio 组件用于播放音频文件,支持多种音频格式。

主要属性

属性名类型默认值说明
idString-音频组件唯一标识符
srcString-音频资源地址
loopBooleanfalse是否循环播放
controlsBooleanfalse是否显示默认控件
posterString-音频封面图片地址
nameString未知音频音频名称
authorString未知作者作者名称
autoplayBooleanfalse是否自动播放

使用示例

基础音频播放

vue
<template>
  <view class="container">
    <audio
      id="myAudio"
      src="https://example.com/audio.mp3"
      controls
      poster="https://example.com/cover.jpg"
      name="示例音频"
      author="示例作者"
      @play="onAudioPlay"
      @pause="onAudioPause"
      @ended="onAudioEnded"
      @timeupdate="onAudioTimeUpdate"
      @error="onAudioError"
    />
  </view>
</template>

<script>
export default {
  data() {
    return {
      audioContext: null
    }
  },
  onReady() {
    this.audioContext = uni.createAudioContext('myAudio', this)
  },
  methods: {
    onAudioPlay() {
      console.log('音频开始播放')
    },
    onAudioPause() {
      console.log('音频暂停')
    },
    onAudioEnded() {
      console.log('音频播放结束')
    },
    onAudioTimeUpdate(e) {
      console.log('播放进度', e.detail.currentTime, e.detail.duration)
    },
    onAudioError(e) {
      console.error('音频播放错误', e.detail)
    }
  }
}
</script>

自定义音频播放器

vue
<template>
  <view class="container">
    <!-- 隐藏的audio组件 -->
    <audio
      id="customAudio"
      :src="currentAudio.src"
      @play="onPlay"
      @pause="onPause"
      @ended="onEnded"
      @timeupdate="onTimeUpdate"
      style="display: none;"
    />
    
    <!-- 自定义播放器界面 -->
    <view class="custom-player">
      <view class="audio-info">
        <image 
          :src="currentAudio.poster" 
          class="audio-cover"
          mode="aspectFill"
        />
        <view class="audio-details">
          <text class="audio-name">{{ currentAudio.name }}</text>
          <text class="audio-author">{{ currentAudio.author }}</text>
        </view>
      </view>
      
      <view class="progress-section">
        <text class="time-text">{{ formatTime(currentTime) }}</text>
        <slider
          :value="progressValue"
          :max="100"
          @change="onProgressChange"
          class="progress-slider"
        />
        <text class="time-text">{{ formatTime(duration) }}</text>
      </view>
      
      <view class="control-buttons">
        <text class="control-btn" @click="prevSong">⏮</text>
        <text 
          class="control-btn play-btn" 
          @click="togglePlay"
        >
          {{ isPlaying ? '⏸' : '▶' }}
        </text>
        <text class="control-btn" @click="nextSong">⏭</text>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      audioContext: null,
      isPlaying: false,
      currentTime: 0,
      duration: 0,
      progressValue: 0,
      currentIndex: 0,
      playlist: [
        {
          src: 'https://example.com/song1.mp3',
          name: '歌曲1',
          author: '歌手1',
          poster: 'https://example.com/cover1.jpg'
        },
        {
          src: 'https://example.com/song2.mp3',
          name: '歌曲2',
          author: '歌手2',
          poster: 'https://example.com/cover2.jpg'
        }
      ]
    }
  },
  computed: {
    currentAudio() {
      return this.playlist[this.currentIndex] || {}
    }
  },
  onReady() {
    this.audioContext = uni.createAudioContext('customAudio', this)
  },
  methods: {
    onPlay() {
      this.isPlaying = true
    },
    onPause() {
      this.isPlaying = false
    },
    onEnded() {
      this.isPlaying = false
      this.nextSong()
    },
    onTimeUpdate(e) {
      this.currentTime = e.detail.currentTime
      this.duration = e.detail.duration
      
      if (this.duration > 0) {
        this.progressValue = (this.currentTime / this.duration) * 100
      }
    },
    togglePlay() {
      if (this.isPlaying) {
        this.audioContext.pause()
      } else {
        this.audioContext.play()
      }
    },
    prevSong() {
      this.currentIndex = (this.currentIndex - 1 + this.playlist.length) % this.playlist.length
      this.audioContext.play()
    },
    nextSong() {
      this.currentIndex = (this.currentIndex + 1) % this.playlist.length
      this.audioContext.play()
    },
    onProgressChange(e) {
      const value = e.detail.value
      const seekTime = (value / 100) * this.duration
      this.audioContext.seek(seekTime)
    },
    formatTime(time) {
      const minutes = Math.floor(time / 60)
      const seconds = Math.floor(time % 60)
      return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
    }
  }
}
</script>

<style>
.custom-player {
  background: #fff;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.audio-info {
  display: flex;
  align-items: center;
  margin-bottom: 20px;
}
.audio-cover {
  width: 60px;
  height: 60px;
  border-radius: 8px;
  margin-right: 15px;
}
.audio-details {
  flex: 1;
}
.audio-name {
  display: block;
  font-size: 16px;
  font-weight: bold;
  margin-bottom: 5px;
}
.audio-author {
  display: block;
  font-size: 14px;
  color: #666;
}
.progress-section {
  display: flex;
  align-items: center;
  margin-bottom: 20px;
}
.time-text {
  font-size: 12px;
  color: #999;
  width: 40px;
}
.progress-slider {
  flex: 1;
  margin: 0 10px;
}
.control-buttons {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 30px;
}
.control-btn {
  font-size: 24px;
  color: #42b983;
}
.play-btn {
  font-size: 32px;
}
</style>

camera 相机组件

camera 组件用于调用设备摄像头进行拍照或录像。

主要属性

属性名类型默认值说明
modeStringnormal模式:normal(拍照/录像)、scanCode(扫码)
resolutionStringmedium分辨率:low、medium、high
device-positionStringback摄像头:front(前置)、back(后置)
flashStringauto闪光灯:auto、on、off
frame-sizeStringmedium期望的相机帧数据尺寸

使用示例

基础相机功能

vue
<template>
  <view class="container">
    <camera
      class="camera"
      :device-position="devicePosition"
      :flash="flash"
      @error="onCameraError"
      @initdone="onCameraInit"
    />
    
    <view class="camera-controls">
      <button @click="takePhoto" class="control-btn">拍照</button>
      <button @click="startRecord" class="control-btn">开始录像</button>
      <button @click="stopRecord" class="control-btn">停止录像</button>
      <button @click="switchCamera" class="control-btn">切换摄像头</button>
      <button @click="toggleFlash" class="control-btn">
        闪光灯: {{ flash }}
      </button>
    </view>
    
    <!-- 预览区域 -->
    <view class="preview-section" v-if="mediaUrl">
      <text class="preview-title">预览</text>
      <image 
        v-if="mediaType === 'image'" 
        :src="mediaUrl" 
        mode="widthFix"
        class="preview-media"
      />
      <video 
        v-else-if="mediaType === 'video'" 
        :src="mediaUrl" 
        controls
        class="preview-media"
      />
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      cameraContext: null,
      devicePosition: 'back',
      flash: 'auto',
      mediaUrl: '',
      mediaType: '',
      flashModes: ['auto', 'on', 'off']
    }
  },
  onReady() {
    this.cameraContext = uni.createCameraContext()
  },
  methods: {
    onCameraError(e) {
      console.error('相机错误', e.detail)
      uni.showToast({
        title: '相机初始化失败',
        icon: 'none'
      })
    },
    onCameraInit(e) {
      console.log('相机初始化完成', e.detail)
    },
    takePhoto() {
      this.cameraContext.takePhoto({
        quality: 'high',
        success: (res) => {
          this.mediaUrl = res.tempImagePath
          this.mediaType = 'image'
          console.log('拍照成功', res.tempImagePath)
        },
        fail: (err) => {
          console.error('拍照失败', err)
          uni.showToast({
            title: '拍照失败',
            icon: 'none'
          })
        }
      })
    },
    startRecord() {
      this.cameraContext.startRecord({
        success: () => {
          console.log('开始录像')
          uni.showToast({
            title: '开始录像',
            icon: 'none'
          })
        },
        fail: (err) => {
          console.error('开始录像失败', err)
        }
      })
    },
    stopRecord() {
      this.cameraContext.stopRecord({
        success: (res) => {
          this.mediaUrl = res.tempVideoPath
          this.mediaType = 'video'
          console.log('录像完成', res.tempVideoPath)
          uni.showToast({
            title: '录像完成',
            icon: 'success'
          })
        },
        fail: (err) => {
          console.error('停止录像失败', err)
        }
      })
    },
    switchCamera() {
      this.devicePosition = this.devicePosition === 'back' ? 'front' : 'back'
    },
    toggleFlash() {
      const currentIndex = this.flashModes.indexOf(this.flash)
      this.flash = this.flashModes[(currentIndex + 1) % this.flashModes.length]
    }
  }
}
</script>

<style>
.container {
  padding: 20px;
}
.camera {
  width: 100%;
  height: 300px;
  border-radius: 8px;
  background-color: #000;
}
.camera-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 20px 0;
}
.control-btn {
  flex: 1;
  min-width: 100px;
  padding: 10px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 14px;
}
.preview-section {
  margin-top: 20px;
}
.preview-title {
  display: block;
  font-size: 16px;
  font-weight: bold;
  margin-bottom: 10px;
}
.preview-media {
  width: 100%;
  border-radius: 8px;
}
</style>

扫码功能

vue
<template>
  <view class="container">
    <camera
      class="camera"
      mode="scanCode"
      @scancode="onScanCode"
      @error="onCameraError"
    />
    
    <view class="scan-result" v-if="scanResult">
      <text class="result-title">扫码结果:</text>
      <text class="result-content">{{ scanResult }}</text>
      <button @click="clearResult" class="clear-btn">清除结果</button>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      scanResult: ''
    }
  },
  methods: {
    onScanCode(e) {
      this.scanResult = e.detail.result
      console.log('扫码结果', e.detail.result)
      
      uni.showToast({
        title: '扫码成功',
        icon: 'success'
      })
    },
    onCameraError(e) {
      console.error('相机错误', e.detail)
    },
    clearResult() {
      this.scanResult = ''
    }
  }
}
</script>

<style>
.scan-result {
  margin-top: 20px;
  padding: 15px;
  background-color: #f8f9fa;
  border-radius: 8px;
}
.result-title {
  display: block;
  font-weight: bold;
  margin-bottom: 10px;
}
.result-content {
  display: block;
  word-break: break-all;
  margin-bottom: 15px;
  color: #333;
}
.clear-btn {
  background-color: #dc3545;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
}
</style>

live-player 直播播放组件

live-player 组件用于实时音视频播放,支持 RTMP、HTTP-FLV 等协议。

主要属性

属性名类型默认值说明
srcString-音视频地址
modeStringlive模式:live(直播)、RTC(实时通话)
autoplayBooleanfalse自动播放
mutedBooleanfalse是否静音
orientationStringvertical画面方向:vertical、horizontal
object-fitStringcontain填充模式:contain、fillCrop
background-muteBooleanfalse进入后台时是否静音
min-cacheNumber1最小缓冲区(秒)
max-cacheNumber3最大缓冲区(秒)

使用示例

vue
<template>
  <view class="container">
    <live-player
      id="livePlayer"
      :src="liveUrl"
      mode="live"
      :autoplay="true"
      :muted="isMuted"
      :orientation="orientation"
      object-fit="contain"
      @statechange="onStateChange"
      @netstatus="onNetStatus"
      @error="onPlayerError"
      class="live-player"
    />
    
    <view class="player-info">
      <text class="info-item">状态: {{ playerState }}</text>
      <text class="info-item">网络: {{ netStatus }}</text>
    </view>
    
    <view class="player-controls">
      <button @click="play" class="control-btn">播放</button>
      <button @click="pause" class="control-btn">暂停</button>
      <button @click="stop" class="control-btn">停止</button>
      <button @click="toggleMute" class="control-btn">
        {{ isMuted ? '取消静音' : '静音' }}
      </button>
      <button @click="toggleOrientation" class="control-btn">
        {{ orientation === 'vertical' ? '横屏' : '竖屏' }}
      </button>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      livePlayerContext: null,
      liveUrl: 'https://example.com/live.flv',
      isMuted: false,
      orientation: 'vertical',
      playerState: '未知',
      netStatus: '未知'
    }
  },
  onReady() {
    this.livePlayerContext = uni.createLivePlayerContext('livePlayer', this)
  },
  methods: {
    onStateChange(e) {
      const stateMap = {
        2001: '已连接服务器',
        2002: '已连接服务器,开始拉流',
        2003: '网络接收到首个视频数据包',
        2004: '视频播放开始',
        2005: '视频播放进度',
        2006: '视频播放结束',
        2007: '视频播放Loading',
        2008: '解码器启动',
        2009: '视频分辨率改变',
        '-2301': '网络断连,且经多次重连抢救无效',
        '-2302': '获取加速拉流地址失败',
        '2101': '当前视频帧解码失败',
        '2102': '当前音频帧解码失败',
        '2103': '网络断连, 已启动自动重连',
        '2104': '网络来包不稳',
        '2105': '当前视频播放出现卡顿',
        '2106': '硬解启动失败,采用软解',
        '2107': '当前视频帧不连续,可能丢帧',
        '2108': '当前流硬解第一个I帧失败,SDK自动切软解'
      }
      
      this.playerState = stateMap[e.detail.code] || `未知状态: ${e.detail.code}`
      console.log('播放状态变化', e.detail.code, this.playerState)
    },
    onNetStatus(e) {
      // 网络状态信息
      const info = e.detail.info
      this.netStatus = `带宽: ${info.videoBitrate || 0}kbps`
      console.log('网络状态', info)
    },
    onPlayerError(e) {
      console.error('直播播放错误', e.detail)
      uni.showToast({
        title: '播放失败',
        icon: 'none'
      })
    },
    play() {
      this.livePlayerContext.play()
    },
    pause() {
      this.livePlayerContext.pause()
    },
    stop() {
      this.livePlayerContext.stop()
    },
    toggleMute() {
      this.isMuted = !this.isMuted
    },
    toggleOrientation() {
      this.orientation = this.orientation === 'vertical' ? 'horizontal' : 'vertical'
    }
  }
}
</script>

<style>
.container {
  padding: 20px;
}
.live-player {
  width: 100%;
  height: 200px;
  background-color: #000;
  border-radius: 8px;
}
.player-info {
  display: flex;
  justify-content: space-between;
  margin: 15px 0;
  padding: 10px;
  background-color: #f8f9fa;
  border-radius: 4px;
}
.info-item {
  font-size: 14px;
  color: #666;
}
.player-controls {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}
.control-btn {
  flex: 1;
  min-width: 80px;
  padding: 8px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 14px;
}
</style>

live-pusher 直播推流组件

live-pusher 组件用于实时音视频录制和推流,支持 RTMP 推流协议。

主要属性

属性名类型默认值说明
urlString-推流地址
modeStringRTC模式:SD(标清)、HD(高清)、FHD(超清)、RTC(实时通话)
autopushBooleanfalse自动推流
mutedBooleanfalse是否静音
enable-cameraBooleantrue开启摄像头
auto-focusBooleantrue自动聚焦
orientationStringvertical画面方向:vertical、horizontal
beautyNumber0美颜,取值范围 0-9
whitenessNumber0美白,取值范围 0-9
aspectString9:16宽高比:3:4、9:16
min-bitrateNumber200最小码率
max-bitrateNumber1000最大码率
audio-qualityStringhigh音频质量:high、low
device-positionStringfront摄像头:front、back
remote-mirrorBooleanfalse设置推流画面是否镜像
local-mirrorStringauto控制本地预览画面是否镜像

使用示例

vue
<template>
  <view class="container">
    <live-pusher
      id="livePusher"
      :url="pushUrl"
      mode="RTC"
      :autopush="false"
      :muted="isMuted"
      :enable-camera="enableCamera"
      :device-position="devicePosition"
      :beauty="beauty"
      :whiteness="whiteness"
      orientation="vertical"
      @statechange="onPushStateChange"
      @netstatus="onPushNetStatus"
      @error="onPushError"
      class="live-pusher"
    />
    
    <view class="pusher-info">
      <text class="info-item">推流状态: {{ pushState }}</text>
      <text class="info-item">网络状态: {{ pushNetStatus }}</text>
    </view>
    
    <view class="pusher-controls">
      <button @click="startPush" class="control-btn">开始推流</button>
      <button @click="stopPush" class="control-btn">停止推流</button>
      <button @click="pausePush" class="control-btn">暂停推流</button>
      <button @click="resumePush" class="control-btn">恢复推流</button>
      <button @click="switchCamera" class="control-btn">切换摄像头</button>
      <button @click="toggleMute" class="control-btn">
        {{ isMuted ? '取消静音' : '静音' }}
      </button>
    </view>
    
    <view class="beauty-controls">
      <view class="beauty-item">
        <text>美颜: {{ beauty }}</text>
        <slider 
          :value="beauty" 
          :max="9" 
          :min="0" 
          @change="onBeautyChange"
          class="beauty-slider"
        />
      </view>
      <view class="beauty-item">
        <text>美白: {{ whiteness }}</text>
        <slider 
          :value="whiteness" 
          :max="9" 
          :min="0" 
          @change="onWhitenessChange"
          class="beauty-slider"
        />
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      livePusherContext: null,
      pushUrl: 'rtmp://example.com/live/stream',
      isMuted: false,
      enableCamera: true,
      devicePosition: 'front',
      beauty: 0,
      whiteness: 0,
      pushState: '未知',
      pushNetStatus: '未知'
    }
  },
  onReady() {
    this.livePusherContext = uni.createLivePusherContext('livePusher', this)
  },
  methods: {
    onPushStateChange(e) {
      const stateMap = {
        1001: '已经连接推流服务器',
        1002: '已经与服务器握手完毕,开始推流',
        1003: '打开摄像头成功',
        1004: '录屏启动成功',
        1005: '推流动态调整分辨率',
        1006: '推流动态调整码率',
        1007: '首帧画面采集完成',
        1008: '编码器启动',
        '-1301': '打开摄像头失败',
        '-1302': '打开麦克风失败',
        '-1303': '视频编码失败',
        '-1304': '音频编码失败',
        '-1305': '不支持的视频分辨率',
        '-1306': '不支持的音频采样率',
        '-1307': '网络断连,且经多次重连抢救无效',
        '-1308': '开始录屏失败,可能是被用户拒绝',
        '-1309': '录屏失败,不支持的Android系统版本,需要5.0以上的系统',
        '-1310': '录屏被其他应用打断了',
        '-1311': 'Android Mic打开成功,但是录不到音频数据',
        '-1312': '录屏动态切横竖屏失败',
        '1101': '网络状况不佳:上行带宽太小,上传数据受阻',
        '1102': '网络断连, 已启动自动重连',
        '1103': '硬编码启动失败,采用软编码',
        '1104': 'VideoToolbox 编码失败',
        '1105': '新美颜软编码启动失败,采用老的软编码',
        '1106': '新美颜软编码启动失败,采用老的软编码',
        '3001': 'RTMP -DNS解析失败',
        '3002': 'RTMP服务器连接失败',
        '3003': 'RTMP服务器握手失败',
        '3004': 'RTMP服务器主动断开,请检查推流地址的合法性或防盗链有效期',
        '3005': 'RTMP 读/写失败'
      }
      
      this.pushState = stateMap[e.detail.code] || `未知状态: ${e.detail.code}`
      console.log('推流状态变化', e.detail.code, this.pushState)
    },
    onPushNetStatus(e) {
      const info = e.detail.info
      this.pushNetStatus = `上行: ${info.netSpeed || 0}KB/s`
      console.log('推流网络状态', info)
    },
    onPushError(e) {
      console.error('推流错误', e.detail)
      uni.showToast({
        title: '推流失败',
        icon: 'none'
      })
    },
    startPush() {
      this.livePusherContext.start()
    },
    stopPush() {
      this.livePusherContext.stop()
    },
    pausePush() {
      this.livePusherContext.pause()
    },
    resumePush() {
      this.livePusherContext.resume()
    },
    switchCamera() {
      this.livePusherContext.switchCamera()
      this.devicePosition = this.devicePosition === 'front' ? 'back' : 'front'
    },
    toggleMute() {
      this.isMuted = !this.isMuted
    },
    onBeautyChange(e) {
      this.beauty = e.detail.value
    },
    onWhitenessChange(e) {
      this.whiteness = e.detail.value
    }
  }
}
</script>

<style>
.live-pusher {
  width: 100%;
  height: 300px;
  background-color: #000;
  border-radius: 8px;
}
.pusher-info {
  display: flex;
  justify-content: space-between;
  margin: 15px 0;
  padding: 10px;
  background-color: #f8f9fa;
  border-radius: 4px;
}
.pusher-controls {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}
.control-btn {
  flex: 1;
  min-width: 80px;
  padding: 8px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 12px;
}
.beauty-controls {
  background-color: #f8f9fa;
  padding: 15px;
  border-radius: 8px;
}
.beauty-item {
  margin-bottom: 15px;
}
.beauty-item text {
  display: block;
  margin-bottom: 8px;
  font-size: 14px;
  color: #333;
}
.beauty-slider {
  width: 100%;
}
</style>

总结

本指南介绍了uni-app中的主要媒体组件:

图片组件

  • image:支持多种格式和缩放模式,提供懒加载功能

音视频组件

  • video:功能丰富的视频播放器,支持弹幕、全屏等功能
  • audio:音频播放组件,可自定义播放器界面

相机组件

  • camera:支持拍照、录像和扫码功能

直播组件

  • live-player:直播播放,支持多种直播协议
  • live-pusher:直播推流,支持美颜和多种推流设置

这些媒体组件为uni-app应用提供了完整的多媒体处理能力,能够满足大部分应用场景的需求。在使用时需要注意各平台的兼容性和权限要求。

一次开发,多端部署 - 让跨平台开发更简单