uniapp APP端视频轮播问题(黑屏,变形)

今天接到一个需求:需要在商品详情轮播展示视频,本以为很简单,但是发现好多坑,在app端出现黑屏,卡顿,变形,视频只展示半屏的情况。
完整代码放底下了

问题1:有声音但是黑屏(所有端都出现)

原因:没有传入poster(封面图)参数,前提 controls 必须为true

问题2:APP端 视频轮播出现变形,走移,卡顿,展示半屏问题

原因:video属于原生标签,层级高,普通标签无法覆盖,在swiper中嵌套video,会受到scroll-view的影响,视频发生错位。

解决方案:大概方案:使用cover-view,在不播放视频以及切换轮播时用cover-view展示封面图,来代替video标签滑动

经过借鉴了一些前辈的代码调好了。特此记录一下

效果展示

uniapp ios video退出全屏有卡顿_ide

完整代码如下

复制即可用

因为视频 和轮播 代码量比较大,把video单独用子组件装起来

uniapp ios video退出全屏有卡顿_uni-app_02

//展示页面
<my-swiper :list="bannerList" :height="height"></my-swiper>

import MySwiper from '@/components/my-swiper/index';
//bannerList:参数  height:轮播高度
[{
poster: "http://oos-cn.ctyunapi.cn/ebusiness/member/1657525898058.jpg",
src: "http://oos-cn.ctyunapi.cn/ebusiness/member/1659077187102.mp4",
type: "video"
},
{src: "http://oos-cn.ctyunapi.cn/ebusiness/member/1657525559014.jpg",
type: "image"
}]

components/my-swiper/index

<template> 
	<view class="swiperContent">    
		<swiper  
			@transition='transition'
            @change="change"  
            @animationfinish="animationfinish"
            :indicator-dots="indicatorDots"
            :indicator-active-color="indicatorActiveColor"
            :indicator-color="indicatorColor"
            :autoplay="autoplay && !videoPlaySataus"
            :current="activeCurrent"
            :interval="interval"
            :vertical="vertical"
            :style="{'height':swiperHeight+'px'}"
            class="z-screen-swiper">  
			<swiper-item 
                class="z-swiper-item"   
                v-for="(item,index) in swiperList"
                :key="index" 
                @tap="clickItem(index)">  
                <view class="swiper-box" :style="{'backgroundColor':`${bgColorItem}`}"> 
                    <view class="swiper-content"> 
                        <slot :row='item' :index='index'>  
                            <image v-if="item[fileType]==='image'"  :src="item[imageKey]" mode='scaleToFill'>   
							<template v-else-if="item[fileType]==='video'"> 
								<!-- :style="{'height':fullScreen ? '':'100%' }" 解决非全屏video显示问题 -->
								<zVideo
								:ignoreTip='false'
								:ref='`video_${index}`'
								:index='index'
								:src="item[videoKey]"
								:poster="item.poster"
								:moveX='moveX' 
								:initial-time="item.currentTime || 0 "
								@play="stopSwiperAuto"
								></zVideo>
							</template>
                        </slot> 
                    </view>

                </view>
			</swiper-item> 
		</swiper>
        <!-- indicatorDots 原生指示器开启时不显示下面自定义指示器 
             vertical为true 垂直方向 只写了支持右侧定位
        -->
        <div 
             v-if="!indicatorDots && list.length>1"
            :class="['dot',vertical ? 'verticalDot':`${indicatorPos}`]"
            > 
            <slot :list='list' :total='list.length' :activeIndex='activeCurrent' name="dot">
                <!-- 指示器自定义-返回列表数据list 数组长度total 选中项索引activeIndex --> 
                <template v-if="mode==='number'">
                    <div>
                        {{activeCurrent+1}}/{{list.length}}
                    </div>
                </template>
                <template v-else> 
                    <div   
                        v-for="(item,index) in list"
                        :key="index"  
                        @click="activeCurrent=index" 
                        :style="{'background-color': activeCurrent==index ? indicatorActiveColor:indicatorColor}"
                        :class="['dotItem',`${mode}`, `${ activeCurrent==index ? 'defautActive':'' }` ]"> 
                        <span v-if="mode ==='index'">{{index+1}}</span> 
                    </div> 
                </template> 
            </slot>
        </div>
	</view>
</template>

<script>
	import zVideo from './video.vue'
	export default {
		name:'my-swiper', 
		components:{zVideo},
		props: {  	   
            list:{//滑块视图容器数据
              type:Array,
              default:_=>[
                  {
					type:'video',  
					poster:'https://img2.baidu.com/it/u=2141851239,1037607188&fm=26&fmt=auto&gp=0.jpg',
					src:'https://bjetxgzv.cdn.bspapp.com/VKCEYUGU-uni-app-doc/360e4b20-4f4b-11eb-8a36-ebb87efcf8c0.mp4',
				  }, 
                  {type:'image', src:'https://img0.baidu.com/it/u=1570602913,157918019&fm=26&fmt=auto&gp=0.jpg'},
                  {type:'image', src:'https://img0.baidu.com/it/u=3464142916,229554071&fm=26&fmt=auto&gp=0.jpg'},
                  {
					type:'video', 
					currentTime:120,//初始帧时间---默认缓存存储
					poster:'https://img1.baidu.com/it/u=1297253752,1185196455&fm=26&fmt=auto&gp=0.jpg',
					src:'https://bjetxgzv.cdn.bspapp.com/VKCEYUGU-uni-app-doc/a876efc0-4f35-11eb-97b7-0dc4655d6e68.mp4',
				  },
                  {type:'image', src:'https://img1.baidu.com/it/u=2057763469,3313822915&fm=26&fmt=auto&gp=0.jpg'},
                  {type:'image', src:'https://img0.baidu.com/it/u=1570602913,157918019&fm=26&fmt=auto&gp=0.jpg'},
              ]  
            },
			fileType: { type: String, default: 'type' }, // 文件类型映射
			videoKey:{ type:String, default:'src'},// 视频映射的键
			imageKey:{ type:String, default:'src'},//图片映射的键
            indicatorPos:{ type:String, default:'bottomCenter'},//指示器的位置:topLeft/topCenter/topRight/bottomLeft/bottomCenter/bottomRight
            mode:{ type:String, default:'round' },//指示器样式: default  circle  round index number none时不显示
            fullScreen:{ type:Boolean, default:false }, //是否全屏
            navHeight:{type:Number, default:44},//顶部导航高度,默认44---垂直全屏状态无导航栏可设置为0
            height:{ type:Number, default:160 },//swiper 高度单位px
			contentRadius:{ type:String, default:'0rpx' },//盒子圆角设置
            // topFloat:{ type:Boolean, default:true },//顶部不占位-浮动定位
            // fotterFloat:{ type:Boolean, default:true },//底部不占位-浮动定位
            bgColor:{type: String, default: '#f3f4f6'},//swiper背景色
            bgColorItem:{type: String, default: 'rgba(0,0,0,0)'},//swiper当前项背景色
			
			//顶部与底部设置-注:-顶部与底部根据需求自己拓展---也可用插槽自定义内容
			topTextKey:{  type:String, default:'topTip'},//顶部文字说明映射的键
			topColor:{ type:String, default:'#FFF'},//顶部文字颜色
			topBackground:{  type:String, default:'rgba(0, 0, 0, 0)'},//顶部背景色
			topTextAlign:{ type:String, default:'left'},//顶部文字位置
			bottomTextKey:{  type:String, default:'bottomTip'},//底部文字说明映射的键
			bottomColor:{ type:String, default:'#00F'},//底部文字颜色
			bottomBackground:{  type:String, default:'rgba(0, 0, 0, 0)'},//底部背景色
			bottomTextAlign:{ type:String, default:'left'},//底部文字位置
			//---end
			nextMargin:{ // 后边距,可用于露出后一项的一小部分,接受 px 和 rpx 值 头条小程序不支持
				type:String,
				default:'0rpx'
			},
			previousMargin:{//前边距,可用于露出前一项的一小部分,接受 px 和 rpx 值头条小程序不支持
				type:String,
				default:'0rpx'
			},
			vertical:{ type:Boolean, default:false },//滑动方向是否为纵向 卡牌不支持纵向以及同时显示的2块以上滑块数量
			interval:{ type:Number, default:2500 },// 自动切换时间间隔
			current:{ type:Number, default:0 },// 初始化时,默认显示第几项
			autoplay:{ type:Boolean, default:true },// 是否自动切换
			indicatorDots: { type: Boolean, default: false },//是否显示面板指示点--默认关闭使用自定义指示器mode设置指示器,原生指示器为true时 则不显示自定义指示器 
			indicatorColor:{ type:String, default:'rgba(0,0,0,0.3)' },// 指示点颜色
			indicatorActiveColor: { type: String, default: '#F1F1F1' },// 选中项指示点颜色
		},
		data() {
			return {
				swiperList:[],//列表数据
				videoContent:'',//视频实例
				videoPlaySataus:false, //视频播放状态---默认禁用
                activeCurrent:0,//当前选中索引
                swiperHeight:0, //轮播图高度
				moveX:0, 
			}
		}, 
        watch: {
            height:{//swiper高度
                handler(newValue) {
                    this.swiperHeight = newValue
                },
                immediate:true
            },
            current:{//初始化选中项
                handler(newValue) {
                    this.activeCurrent = newValue
                },
                immediate:true
            },
			list:{//初始化数据列表--- 处理vue不能直接改变prpos属性
                handler(newValue) {
                    this.swiperList = newValue || []
                },
                immediate:true
            },
			fullScreen:{
				handler(newValue) {
				    if(this.fullScreen){//全屏设置---默认初始化设置一次
				        uni.getSystemInfo({
				            success:(e)=>{
				                console.log('e',e);
				    			this.swiperHeight = e.screenHeight - this.navHeight
				    			// #ifdef APP-PLUS || MP-WEIXIN
				    				this.swiperHeight = e.screenHeight - this.navHeight - e.statusBarHeight  
				    			// #endif  
				            }
				        })
				    }else{
						this.swiperHeight = this.height
					}
				},
				immediate:true
			}
        }, 
		mounted(){
			
		},
		methods: { 
			// 播放视频时让轮播停止自动轮播
			stopSwiperAuto(e){
				console.log("111111111",e)
				this.videoPlaySataus = e
			},
			timeupdate(e){//播放进度变化时触发--更新播放缓存   
				this.$set(this.swiperList[this.activeCurrent],'currentTime',e.detail.currentTime)
			},
			clickItem(index){
				if(this.list.length>0){
					this.$emit('clickItem',this.list[index])
				}
            },   
			change(e){//轮播改变触发   
				try{// 切换前暂停之前视频 
					let preSwiper = this.swiperList[this.activeCurrent] 
					if(preSwiper[this.fileType]==='video'){ 
						// uni.createVideoContext(`video_${this.activeCurrent}`,this).pause();
						this.$refs[`video_${this.activeCurrent}`][0].pausePlay()  
					} 
				}catch(e){
					//TODO handle the exception
				}   
				this.videoPlaySataus = false //自动切换关闭视频播放状态 
                this.activeCurrent = e.detail.current;  
				this.$emit('change',e) 
			},
			animationfinish(e){//动画结束后调用    
				this.moveX = 0
				this.$emit('animationfinish',e)  
			},
			transition(e){//滑动
				// #ifdef APP-PLUS
					this.moveX = e.detail.dx 
				// #endif
			},
			touchStart(e){//触摸
				console.log("e触摸: ",e);
			},
			touchEnd(e){//触摸
				console.log("e触摸结束: ",e);
			},
			clickCover(e){
				console.log("点击: ",e);  
			},
			touchmove(e){
				console.log("滑动中: ",e);
			}
		}
	}
</script> 
<style lang="scss" scoped>
.swiperContent{//容器
    width:100%;
    position: relative;
    // background-color: #ccc;  
    .z-screen-swiper {//轮播图
        min-height: 320rpx; 
        // background-color: rgb(211, 235, 107); //--调试样式
        box-sizing: border-box;
        .z-swiper-item{ 
            box-sizing: border-box;
            overflow: initial;  
            .swiper-box{//轮播图内容
                // background-color: #e7ca8f;//--调试样式
                // background-color: rgba(0,0,0,0.1);//swiper当前项 背景色---已改为配置
                box-sizing: border-box;
                width: 100%;
                height: 100%;
                overflow: hidden;
                display: flex;
                flex-direction: column; 
                justify-content: space-between;
                position: relative;
                color: #FFF;
                .swiper-top{
                    top: 0;
                    // background-color: rgba(0,0,0,0.2);//背景色---已改为配置
                }
                .swiper-content{
                    // background-color: rgb(61, 41, 175);   
                    flex: 1;  
					display: flex;
					flex-direction: column;
					justify-content: center;
					image{
						width:100%;
						height: 100%;
						max-height: 100%; 
					}
					video {// 视频默认不全屏高度---防止全屏swiper滑动切换
						width: 100%;   
						height: 100%; 
						// height: 750rpx;
                        // pointer-events: auto;  
					} 
                } 
                .swiper-fotter{
                    bottom: 0;
                    // background-color: rgba(0, 0, 0, 0.2); //背景色---已改为配置
                } 
                .isFloat{//是否浮动 顶部、底部定位
                    position: absolute;
                    left: 0;
                    right: 0; 
                    z-index: 999;
                }
            }  
        }
    }
    // .effect3D{//3d模式样式 
    //     .z-swiper-item{//3d模式基础样式  
    //         .swiper-box{  
    //             border-radius: 10rpx; 
    //             opacity: 0.7;
    //             transition: all 0.1s ease-in 0s;  
    //         } 
    //     }  
    //     &.effect3D-X{ 
    //         .z-swiper-item{ //选项卡间隔 
    //             padding: 0 10rpx;   
    //         } 
    //         .swiper-box{   
    //             transform: scale(1,0.9);   
    //         }    
    //     } 
    //     &.effect3D-Y{ 
    //         .z-swiper-item{ //选项卡间隔 
    //             padding:10rpx 0;   
    //         } 
    //         .swiper-box{   
    //             transform: scale(0.9,1);   
    //         }    
    //     } 
    //     .active-swiper{//选中样式恢复 
    //         .swiper-box{
    //             transform:initial; 
    //             opacity: 1; 
    //             transition: all 0.1s ease-in 0s;
    //         }
    //     }
       
    // }

    .dot{//指示器
        position: absolute; 
        z-index: 9999;  
        display: flex; 
        color: #FFF;   
        .dotItem{//指示器 颜色与形状 
            background-color: #fff;   
			font-size: 24rpx;
			color: #e2e2e2;
            margin-right: 10rpx;
            &.default{ /*默认条状 */
                height: 8rpx;
                width: 40rpx;
            }
            &.circle{ /* 圆 */
                height: 20rpx;
                width: 20rpx;  
                border-radius: 50%;
            }
            &.index{ /* 数字索引 */
                height: 30rpx;
                width: 30rpx; 
                display: flex;
                justify-content: center;
                align-content: center;
                border-radius: 50%;
            } 
            &.defautActive{//选中项设置
                transition: background-color 0.3s ease-out 0s; //选中动画
            }
            &.round{ /* 弧形 */
                height: 20rpx;
                width: 20rpx;  
                border-radius: 50%;
                &.defautActive{//弧形选中项设置 
                     width: 60rpx;
                     height: 20rpx; 
                     border-radius: 10px; 
                     transition: background-color 0.3s ease-out 0s; //
                }
            } 
        } 
        // 定位位置
        &.verticalDot{//垂直方向 只写了支持右侧定位
            right: 20rpx;
            top: 50%;
            transform: translateY(-50%);
            display: block; 
            margin: 0 !important; 
            .dotItem{
                margin: 0;
                margin-bottom: 10rpx;
                &.round{ /* 弧形 */
                    height: 20rpx;
                    width: 20rpx;  
                    border-radius: 50%;
                    &.defautActive{//弧形选中  
                        width: 20rpx;
                        height: 60rpx; 
                        border-radius: 10px;
                    }
                } 
            }
        }
        &.bottomLeft{//左上角
            left: 20rpx;
            bottom: 20rpx;  
        }
        &.bottomCenter{//
            left: 50%;
            bottom: 20rpx; 
            transform: translateX(-50%);
        }
        &.bottomRight{
            right: 20rpx;
            bottom: 20rpx;  
        }
        &.topLeft{
            left: 20rpx;
            top: 10rpx;  
        }
        &.topCenter{
            left: 50%;
            top: 10rpx;  
            transform: translateX(-50%);
        }
        &.topRight{
            right: 20rpx;
            top: 10rpx; 
        }
    }
} 
</style>
<style lang="scss" scoped>
#coverViewVideo{
	height: 200rpx;
	background-color: #ccc;
	width: 100%; 
}
</style>

components/my-swiper/video

uni.createVideoContext(id,this); //this一定要加,否则在微信小程序端有问题

<template>
	<!-- 视频单项 -->
	<view class="video-layout">
		<video
			v-show="isplay"
			:id="`myVideo${index}`"
			:style="{ height: height, width: '100%' }"
			:src="src"
			controls
			objectFit="contain"
			:enable-progress-gesture="enableProgressGesture"
			x5-video-player-type="h5"
			x-webkit-airplay="allow"
			webkit-playsinline="true"
			@error="videoErrorCallback"
			@play="play"
			@pause="pause"
		>
			<!-- #ifdef APP-PLUS -->
			<cover-view :style="{ transform: 'translateX(' + moveX + 'px)' }" />
			<!-- #endif -->
			<!-- 不使用弹窗提示,视频内部提示可使用cover-view自定义播放提示样式 -->
		</video>
		<view v-show="!isplay" class="image" :style="{ height: height, width: '100%' }" @click="beforePlay">
			<image mode="aspectFill" :src="poster" />
			<view class="play" />
		</view>
	</view>
</template>
<script>
// enable-progress-gesture 手势滑动在非app页面开启后,视频轮播会存在与轮播图,滑动事件同时触发的情况
export default {
	props: {
		moveX: { type: [Number, String], default: 0 }, // 轮播图兼容滑动兼容-单独使用可不传
		index: { type: [Number, String], default: 0 }, // 下标索引
		height: { type: String, default: '750rpx' }, // 视频高度
		borderRadius: { type: Number, default: 0 }, // 圆角值,单位rpx
		videoSize: { type: [Number, String], default: 10 }, // 视频大小
		ignoreTip: { type: Boolean, default: true }, // 播放环境提示
		// #ifdef APP-PLUS
		enableProgressGesture: { type: Boolean, default: true }, // 手势滑动
		// #endif
		// #ifndef APP-PLUS
		enableProgressGesture: { type: Boolean, default: false }, // 手势滑动
		// #endif
		src: {
			// 播放地址
			type: String,
			default: 'https://bjetxgzv.cdn.bspapp.com/VKCEYUGU-uni-app-doc/360e4b20-4f4b-11eb-8a36-ebb87efcf8c0.mp4'
		},
		poster: {
			// 封面
			type: String,
			default: 'https://img1.baidu.com/it/u=1601695551,235775011&fm=26&fmt=auto'
		}
	},
	data() {
		return {
			//   videoContext: null, // 视频实例
			isplay: false, // 播放状态
			isTip: true // 是否提示
		};
	},
	watch: {
		ignoreTip: {
			handler(v) {
				this.isTip = v;
			},
			immediate: true
		}
	},
	onReady() {
		// this.videoContext = uni.createVideoContext('myVideo')
	},
	methods: {
		videoErrorCallback: function(e) {
			console.log('视频错误信息:');
			console.log(e.target.errMsg);
		},
		play() {
			console.log('播放');
			this.$emit('play', true);
		},
		pause() {
			console.log('暂停');
			this.isplay = false;
			this.$emit('play', false);
		},
		startPlay() {
			// 开始播放
			this.isplay = true;
			this.$nextTick(() => {
				const id = `myVideo${this.index}`;
				//一定要加this,不然微信小程序有问题(需要需要触发两次才能play)
				const video = uni.createVideoContext(id,this);
				video.play();
			});
		},
		pausePlay() {
			//暂停播放
			const id = `myVideo${this.index}`;
			//一定要加this
			const video = uni.createVideoContext(id,this);
			video.pause();
			this.isplay = false;
		},
		beforePlay() {
			// 播放前拦截
			this.isplay = true; // 拦截前显示播放视频
			if (!this.isTip) return this.startPlay(); // 不提示直接播放
			// https://uniapp.dcloud.io/api/system/network?id=getnetworktype
			uni.getNetworkType({
				success: res => {
					const networkType = res.networkType;
					if (networkType === 'wifi' || networkType === 'ethernet') {
						this.startPlay();
					} else {
						uni.showModal({
							title: '提示',
							content: `当前为移动网络,播放视频需消耗${this.videoSize}M流量,是否继续播放?`,
							success: res => {
								if (res.confirm) {
									this.startPlay();
									this.isTip = false;
								} else {
									this.isplay = false;
								}
							}
						});
					}
				}
			});
		}
	}
};
</script>
<style lang="scss" scoped>
.video-layout {
	display: flex;
	align-items: center;
	// video{
	//     width: 100%;
	//     height: 100%;
	//     /deep/.uni-video-container{
	//         width: auto;
	//         height: auto;
	//     }
	// }
	.image {
		position: relative;
		width: 100%;
		height: 100%;
		/deep/uni-image {
			width: 100%;
			height: 100%;
		}
		.play {
			position: absolute;
			left: 50%;
			top: 50%;
			width: 80rpx;
			height: 80rpx;
			transform: translate(-50%, -50%);
			// background-image: url('@/static/play.png');
			background-image: url('https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fbpic.588ku.com%2Felement_pic%2F20%2F07%2F01%2F215e4e632b7438794f5b75fe8ad35388.jpg%21%2Ffw%2F253%2Fquality%2F90%2Funsharp%2Ftrue%2Fcompress%2Ftrue&refer=http%3A%2F%2Fbpic.588ku.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1661933376&t=47385c16830ca399e0873f50474fbeeb');
			background-size: contain;
			background-repeat: no-repeat;
			background-color: rgba($color: #000000, $alpha: 0.1);
			border-radius: 50%;
		}
	}
}
</style>