更新记录

1.0.3(2024-12-06) 下载此版本

  • 新增视频控制条,支持播放、暂停、快进和快退。
  • 新增 updatePipActions API,用于更新画中画的播放/暂停操作按钮。
  • 新增 onPipActions API,用于监听画中画的播放/暂停/快进/快退操作按钮点击事件。
  • 新增 onUserLeaveHint API,用于监听用户离开当前页面时触发的回调。
  • StartOptions 新增 isPlaying 是否正在播放参数,用于切换画中画播放暂停按钮状态。
  • 新增 绑定插件 kux-broadcast-receiver,用于监听系统广播。

1.0.2(2024-08-28) 下载此版本

  • 修复新版编译器报错 activity 找不到的问题。
  • 新增 checkPermission API,用于检查是否获取了权限。
  • 新增 checkSupportPIP API,用于检查当前设备系统是否支持PIP画中画功能。
  • 错误码新增 1003,表示当前设备不支持PIP画中画功能。
  • 优化其他已知问题。

1.0.1(2024-03-08) 下载此版本

优化错误反馈,新增源错误返回

查看更多

平台兼容性

Vue2 Vue3
App 快应用 微信小程序 支付宝小程序 百度小程序 字节小程序 QQ小程序
HBuilderX 3.6.8,Android:8.0,iOS:不确定,HarmonyNext:不确定 × × × × × ×
钉钉小程序 快手小程序 飞书小程序 京东小程序 鸿蒙元服务
× × × × ×
H5-Safari Android Browser 微信浏览器(Android) QQ浏览器(Android) Chrome IE Edge Firefox PC-Safari
× × × × × × × × ×

kux-pip

kux-pip 是一个原生画中画的简单封装实现,可以实现简单类似视频软件的小窗播放效果。支持画中画窗口变化监听、自定义宽高比等。

开源地址:https://gitcode.com/kviewui/kux-pip

插件特色

  • 原生画中画实现
  • 窗口变化监听
  • 自定义宽高比
  • 同时支持 uniapp 项目和 uniapp x 项目

目录结构

  1. 基础
    1. 安装配置
    2. 入门使用
  2. API
    1. start
    2. checkPermission
    3. checkSupportPIP
    4. updatePipActions
    5. onPipActions
    6. onUserLeaveHint
  3. 自定义类型
    1. StartOptions
  4. 错误码

基础

安装配置

本插件为完全的 uni_modules 插件,所以直接在 插件市场 搜索 kux-pip 安装即可。

入门使用

注意

需要打包自定义基座方可正常使用

uniapp x项目示例

<template>
    <!-- #ifdef APP -->
    <scroll-view style="flex:1">
    <!-- #endif -->
        <!-- 状态栏 -->
        <view v-if="height == initHeight" class="status_bar"></view>
        <view>
            <video ref="videoRef" style="width: 100%;" :style="{height: height + 'px'}" :controls="controls" src="http://www.runoob.com/try/demo_source/mov_bbb.mp4" @play="onPlay" @pause="onPause" @ended="" @timeupdate=""></video>
        </view>
        <button @tap="enterPictureInPictureMode">开启画中画模式</button>
        <button @tap="onCheckPermission">检查权限是否被授予</button>
        <button @tap="onCheckSupportPIP">检查是否支持画中画.</button>
    <!-- #ifdef APP -->
    </scroll-view>
    <!-- #endif -->
</template>

<script setup>
    import { start, StartOptions, StartSuccess, checkPermission, checkSupportPIP, onUserLeaveHint, updatePipActions, onPipActions  } from '@/uni_modules/kux-pip';

    const videoRef = ref<UniVideoElement | null>(null);

    const initHeight = ref(200);
    const controls = ref(true);
    const height = ref(initHeight.value);
    const isPlaying = ref(false);
    const time = ref(0);

    /**
     * 更新播放状态以及刷新画中画按钮状态
     */
    const updatePlayState = (state: boolean) => {
        updatePipActions({
            isPlaying: state,
            success(res: StartSuccess) {
                console.log('修改成功');
            },
            fail(err: UniError) {
                console.log(err);
            }
        })
    }

    const  = (event: UniVideoTimeUpdateEvent) => {
        time.value = event.detail.currentTime;
    }

    const onPlay = (event: UniEvent) => {
        isPlaying.value = true;
        updatePlayState(isPlaying.value);
    }

    const onPause = (event: UniEvent) => {
        isPlaying.value = false;
        updatePlayState(isPlaying.value);
    }

    const  = (event: UniEvent) => {
        isPlaying.value = false;
        updatePlayState(isPlaying.value);
    }

    const msg = (content: string) => {
        uni.showToast({
            icon: 'none',
            title: content,
        });
    }

    /**
     * 监听画中画按钮点击事件
     */
    onPipActions((state: number) => {
        if (state == 1) {
            msg('点击了播放');
            videoRef.value?.play();
        } else if (state == 2) {
            msg('点击了暂停');
            videoRef.value?.pause();
        } else if (state == 3) {
            msg('快进1秒');
            videoRef.value?.seek(time.value + 1);
        } else if (state == 4) {
            msg('快退1秒');
            videoRef.value?.seek(time.value - 1);
        }
    });

    function enterPictureInPictureMode() {
        start({
            isPlaying: isPlaying.value,
            success(res: StartSuccess) {
                console.log(res, '成功回调');
            },
            fail(res: UniError) {
                console.log(res, '失败回调');
            },
            complete(res: any) {
                console.log(res, '完成回调');
            },
            stateChange(res) {
                if (res.height < initHeight.value) {
                    height.value = res.height;
                    controls.value = false;
                } else {
                    height.value = initHeight.value;
                    controls.value = true;
                }
            }
        } as StartOptions);
    }

    /**
     * 监听用户离开当前页面事件
     */
    onUserLeaveHint((params: UniActivityParams) => {
        // 如果返回页面结果是index2页面,则进入画中画模式
        if (params.pageRoute == 'pages/index2/index2') {
            enterPictureInPictureMode();
        }
    })

    const modal = (message: string) => {
        uni.showModal({
            title: '提示',
            content: message,
            showCancel: false
        })
    }

    const onCheckPermission = () => {
        try {
            if (checkPermission()) {
                modal('权限已获取');
            } else {
                modal('权限未获取');
            }
        } catch (err: UniError) {
            modal(`检查权限失败:${err.errMsg}`);
        }
    }

    const onCheckSupportPIP = () => {
        if (checkSupportPIP()) {
            modal('支持画中画');
        } else {
            modal('不支持画中画');
        }
    }
</script>

<style>
    .status_bar {
        height: var(--status-bar-height);
        width: 100%;
        background-color: black;
    }
</style>

uniapp 项目示例

<template>
    <!-- #ifdef APP -->
    <scroll-view style="flex:1">
    <!-- #endif -->
        <!-- 状态栏 -->
        <view v-if="height == initHeight" class="status_bar"></view>
        <view>
            <video id="video" ref="videoRef" :style="{height: height + 'px', width: width}" :controls="true" src="http://www.runoob.com/try/demo_source/mov_bbb.mp4" @play="onPlay" @pause="onPause" @ended="" @timeupdate=""></video>
        </view>
        <button @click="enterPictureInPictureMode">开启画中画模式</button>
        <button @tap="onCheckPermission">检查权限是否被授予</button>
        <button @tap="onCheckSupportPIP">检查是否支持画中画</button>
    <!-- #ifdef APP -->
    </scroll-view>
    <!-- #endif -->
</template>

<script lang="ts" setup>
    import { ref } from 'vue';
    import { start, StartOptions, StartSuccess, checkPermission, checkSupportPIP, onUserLeaveHint, updatePipActions, onPipActions } from '@/uni_modules/kux-pip';
    import { onReady } from '@dcloudio/uni-app';

    // const videoRef = ref<UniVideoElement | null>(null);

    const initHeight = ref(200);
    const height = ref(initHeight.value);
    const width = ref('100%');
    const controls = ref(true);
    const isPlaying = ref(false);
    const time = ref(0);

    const videoContext = ref(null);

    onReady(() => {
        videoContext.value = uni.createVideoContext('video');
    })

    /**
     * 更新播放状态以及刷新画中画按钮状态
     */
    const updatePlayState = (state: boolean) => {
        updatePipActions({
            isPlaying: state,
            success(res: StartSuccess) {
                console.log('修改成功');
            },
            fail(err: UniError) {
                console.log(err);
            }
        })
    }

    const  = (event: UniVideoTimeUpdateEvent) => {
        time.value = event.detail.currentTime;
    }

    const onPlay = (event: UniEvent) => {
        isPlaying.value = true;
        updatePlayState(isPlaying.value);
    }

    const onPause = (event: UniEvent) => {
        isPlaying.value = false;
        updatePlayState(isPlaying.value);
    }

    const  = (event: UniEvent) => {
        isPlaying.value = false;
        updatePlayState(isPlaying.value);
    }

    const msg = (content: string) => {
        uni.showToast({
            icon: 'none',
            title: content,
        });
    }

    /**
     * 监听画中画按钮点击事件
     */
    onPipActions((state: number) => {
        if (state == 1) {
            msg('点击了播放');
            videoRef.value?.play();
        } else if (state == 2) {
            msg('点击了暂停');
            videoRef.value?.pause();
        } else if (state == 3) {
            msg('快进1秒');
            videoRef.value?.seek(time.value + 1);
        } else if (state == 4) {
            msg('快退1秒');
            videoRef.value?.seek(time.value - 1);
        }
    });

    function enterPictureInPictureMode() {
        start({
            success(res: StartSuccess) {
                console.log(res, '成功回调');
            },
            fail(res) {
                console.log(res, '失败回调');
            },
            complete(res: any) {
                console.log(res, '完成回调');
            },
            stateChange(res) {
                if (res.height < initHeight.value) {
                    height.value = res.height;
                    width.value = `${res.width}px`;
                    controls.value = false;
                } else {
                    height.value = initHeight.value;
                    width.value = '100%';
                    controls.value = true;
                }
            }
        } as StartOptions);
    }

    /**
     * 监听用户离开当前页面事件
     */
    onUserLeaveHint((params: UniActivityParams) => {
        // 如果返回页面结果是index2页面,则进入画中画模式
        if (params.pageRoute == 'pages/index2/index2') {
            enterPictureInPictureMode();
        }
    })

    const modal = (message: string) => {
        uni.showModal({
            title: '提示',
            content: message,
            showCancel: false
        })
    }

    const onCheckPermission = () => {
        try {
            if (checkPermission()) {
                modal('权限已获取');
            } else {
                modal('权限未获取');
            }
        } catch (err: UniError) {
            modal(`检查权限失败:${err.errMsg}`);
        }
    }

    const onCheckSupportPIP = () => {
        if (checkSupportPIP()) {
            modal('支持画中画');
        } else {
            modal('不支持画中画');
        }
    }
</script>

<style>
    .status_bar {
        height: var(--status-bar-height);
        width: 100%;
        background-color: black;
    }
</style>

注意

  • 因为uniapp项目的activity管理和uniapp x的有差异,所以uniapp项目小窗是把整个应用小窗了,这点要特别注意下

  • 需要小窗的页面建议禁用原生导航栏

  • uniapp项目下需要自己调整进入小窗和退出小窗时页面内容的样式布局,可以参考上面示例代码和uniapp x的是有少许的差异

  • 编译器 4.25 及以上版本测试有些问题,报错:uts.sdk.modules.kuxPipIndexKt.startByJs stateChange is not found,该问题后续找官方协助解决

API

start

  • 描述:开启画中画
  • 参数:StartOptions
  • 返回值:void

checkPermission

  • 描述:检查权限是否被授予

  • 参数:void

  • 返回值:boolean

  • 注意事项:

    • 该方法如果权限检查失败会抛出 UniError 异常,所以需要用 try...catch 捕获异常
    • v1.0.2 及以上版本支持
  • 示例:

    try {
        const permission = checkPermission();
        if (permission) {
            console.log('权限已获取');
        } else {
            console.log('权限未获取');
        }
    } catch (err: UniError) {
        console.log(`检查权限失败:${err.errMsg}`);
    }

checkSupportPIP

  • 描述:检查是否支持画中画

  • 参数:void

  • 返回值:boolean

  • 注意事项:

    • v1.0.2 及以上版本支持
  • 示例:

    const support = checkSupportPIP();
    if (support) {
        console.log('支持画中画');
    } else {
        console.log('不支持画中画');
    }

updatePipActions

  • 描述:更新画中画操作按钮
  • 参数:updatePipActions
  • 返回值:void
  • 注意事项:
    • v1.0.3 及以上版本支持

onPipActions

  • 描述:监听画中画操作按钮点击事件
  • 参数:(state: number) => void
  • 返回值:void
  • state: 1 播放,2 暂停,3 快进,4 快退
  • 注意事项:
    • v1.0.3 及以上版本支持

onUserLeaveHint

  • 描述:监听用户离开小窗事件
  • 参数:(params: UniActivityParams) => void
  • 返回值:void
  • 注意事项:
    • v1.0.3 及以上版本支持

updatePipActions参数

参数名 类型 是否必填 默认值 描述
isPlaying boolean false 是否正在播放视频
success (res: StartSuccess) => void - 成功回调
fail (res: UniError) => void - 失败回调
complete (res: any) => void - 完成回调

StartOptions

参数名 类型 是否必填 默认值 描述
numerator number 16 自定义像素比的分子或者长度比例
denominator number 9 自定义像素比的分母或者宽度比例
success (res: StartSuccess) => void - 成功回调
fail (res: UniError) => void - 失败回调
complete (res: any) => void - 完成回调
stateChange (res: StateChange) => void - 窗口变化监听回调

StartSuccess

参数名 类型 描述
errCode number 成功状态码
errMsg string 成功描述

StateChange

参数名 类型 描述
width number 画中画窗口宽度
height number 画中画窗口高度

自定义类型

StartSuccess

export type StartSuccess = {
    errCode: number;
    errMsg: string;
}

StateChange

export type StateChange = {
    width: number;
    height: number;
}

StartOptions

export type StartOptions = {
    numerator?: number;
    denominator?: number;
    success?: (res: StartSuccess) => void;
    fail?: (res: UniError) => void;
    complete?: (res: any) => void;
    stateChange?: (res: StateChange) => void;
}

Start

export type Start = (options: StartOptions) => void;

StartErrorCode

export type StartErrorCode = 1001 | 1002;

StartFail

export interface StartFail extends IUniError {
    errCode: StartErrorCode
}

错误码

错误码 描述
1001 开启画中画失败
1002 用户拒绝了画中画授权
1003 当前系统版本不支持画中画
1004 当前不是画中画状态,无法更新画中画按钮 (v1.0.3新增)

友情推荐

隐私、权限声明

1. 本插件需要申请的系统权限列表:

android.permission.FOREGROUND_SERVICE

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不采集任何数据

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

许可协议

MIT协议

使用中有什么不明白的地方,就向插件作者提问吧~ 我要提问