MediaStream使用webRtc多窗口传递

最近在做音视频通话,有个需求是把当前会话弄到另一个窗口单独展示,但是会话是属于主窗口的,多窗口通信目前不能直接传递对象,所以想着使用webRtc在主窗口和兄弟窗口建立连接,把主窗口建立会话得到的MediaStream传递给兄弟窗口;

主窗口界面

在这里插入图片描述

兄弟窗口界面

在这里插入图片描述

主窗口点击发送本地媒体后

在这里插入图片描述

呼叫封装_主窗口
// 主窗口WebRtc_呼叫
class CallWindowWebRtc {
    // 广播通道
    curBroadcas: BroadcastChannel;
    // webRtc点对点连接
    peerConnection: RTCPeerConnection;
    // 广播通道
    constructor({broadcastChannelName = 'yyh_text'}) {
        this.curBroadcas = CreateBroadcastChannel(broadcastChannelName);
        this.curBroadcas.onmessage = (event) => this.onMessage(event);
        // 处理页面刷新和关闭方法
        this.handlePageRefreshClose();
    }

    // 接收消息
    onMessage(event: any) {
        const msg = event.data;

        // 收到远端接听消息
        if (msg.type === 'answer') {
            this.handleSedRemoteSDP(msg);
        }
        if (msg.type === 'hangup') {
            this.hangup();
        }
    }
    // 发送消息_方法
    postMessage(msg: any) {
        this.curBroadcas.postMessage(msg);
    }
    // 处理页面刷新和关闭方法
    handlePageRefreshClose() {
        window.addEventListener('beforeunload', () => {
            this.postMessage({data: {event: 'mainPageRefresh', eventName: '主页面刷新了'}});
        });
        window.addEventListener('beforeunload', () => {
            this.postMessage({data: {event: 'mainPageClose', eventName: '主页面关闭了'}});
        });
    }
    // 处理媒体停止
    handleStreamStop() {
        if (!this.peerConnection) { return; }
        let localStream = this.peerConnection.getSenders();
        localStream.forEach((item: any) => {
            item.track.stop();
        })
    }
    // 卸载
    unmount() {
        if (this.peerConnection) {
            let localStream = this.peerConnection.getSenders();
            localStream.forEach((item: any) => {
                item.track.stop();
            })
            this.peerConnection.close();
            this.peerConnection.onicecandidate = null;
            this.peerConnection.ontrack = null;
            this.peerConnection.oniceconnectionstatechange = null;
            this.peerConnection = null;
        }
        if (this.curBroadcas) {
            this.curBroadcas.onmessage = null;
            this.curBroadcas = null;
        }
    }
    // ICE连接状态回调
    handleOniceconnectionstatechange(event) {
        // 1.检查网络配置
        if (this.peerConnection.iceConnectionState === 'checking') {
            // 发送订阅消息_给外部
            this.onSubscriptionMsg({event: 'iceConnectionState', code: 'checking', eventName: '检查网络配置'});
            // 2.ICE候选者被交换并成功建立了数据传输通道
        } else if (this.peerConnection.iceConnectionState === 'connected') {
            // 发送订阅消息_给外部
            this.onSubscriptionMsg({event: 'iceConnectionState', code: 'connected', eventName: 'ICE候选者被交换并成功建立了数据传输通道'});
            // 3.当连接被关闭或由于某种原因(如网络故障、对端关闭连接等)中断时
        } else if (this.peerConnection.iceConnectionState === 'disconnected') {
            this.hangup();
            this.onSubscriptionMsg({event: 'iceConnectionState', code: 'connected', eventName: 'ICE接被关闭或由于某种原因断开'});
        };
    }
    // 发送订阅消息给外部
    onSubscriptionMsg(msg: {}) {
    }
    // 创建全新的 RTCPeerConnection
    handleCreateNewPerrc() {
        // 停止媒体 
        this.handleStreamStop();
        // 最好每一次通话都单独创建一个RTCPeerConnection对象,防止复用导致ICE候选的收集受到之前连接的影响,导致画面延迟加载,或其它异常问题无法排查处理;
        this.peerConnection = new RTCPeerConnection();
        this.peerConnection.onicecandidate = (event) => this.onIcecandidate(event);
        this.peerConnection.ontrack = (event) => this.handleOnTrack(event);
        this.peerConnection.oniceconnectionstatechange  = (event) => this.handleOniceconnectionstatechange(event);
    }
    // 呼叫
    call(stream?: MediaStream) {
        return new Promise((resolve, reject) => {
            this.handleCreateNewPerrc();
            this.handleStreamAddPeerConnection(stream);
            this.handleCreateOffer().then((offer) => {
                // 存入本地offer
                this.handleLocalDes(offer).then(() => {
                    // 给远端发sdp
                    this.handleSendSDP();
                    resolve({code: 1, message: '发送sdp给远端'});
                }).catch(() => {
                    reject({code: 0, message: '存入本地offer失败'});
                });
            }).catch(() => {
                reject({code: 0, message: '创建offer失败'});
            });
        });
    }
    // 挂断
    hangup() {
        if (!this.peerConnection) {
            return;
        }
        if (this.peerConnection.signalingState === 'closed') {
            return;
        };
        this.postMessage({type: 'hangup'});
        // 停止媒体流
        let localStream = this.peerConnection.getSenders();
        localStream.forEach((item: any) => {
            item.track.stop();
        });
        // 关闭peerConection
        this.peerConnection.close();

    }
    // 1.获取本地媒体流
    getUserMediaToStream(audio: true, video: true) {
        return navigator.mediaDevices.getUserMedia({audio, video});
    }
    // 2.把媒体流轨道添加到 this.peerConnection 中
    handleStreamAddPeerConnection(stream?: MediaStream) {
        if (!stream) {
            stream = new MediaStream();
        }
        const tmpStream = new MediaStream();
        const audioTracks = stream.getAudioTracks();
        const videoTracks = stream.getVideoTracks();
        if (audioTracks.length) {
            tmpStream.addTrack(audioTracks[0]);
            this.peerConnection.addTrack(tmpStream.getAudioTracks()[0], tmpStream);
        }
        if (videoTracks.length) {
            tmpStream.addTrack(videoTracks[0]);
            this.peerConnection.addTrack(tmpStream.getVideoTracks()[0], tmpStream);
        }
    }
    // 3.创建createOffer
    handleCreateOffer() {
        return this.peerConnection.createOffer();
    }
    // 4.设置本地SDP描述
    handleLocalDes(offer) {
        return this.peerConnection.setLocalDescription(offer);
    }
    // 5.发送SDP消息给远端
    handleSendSDP() {
        if (this.peerConnection.signalingState === 'have-local-offer') {
            // 使用某种方式将offer传递给窗口B
            const answerData = {
                type: this.peerConnection.localDescription.type,
                sdp: this.peerConnection.localDescription.sdp
            };
            this.curBroadcas.postMessage(answerData);
        }
    }
    // 6.收到远端接听_存远端SDP
    handleSedRemoteSDP(msg: any) {
        // if (this.peerConnection.signalingState === 'stable') { return; }
        const answerData = msg;
        const answer = new RTCSessionDescription(answerData);
        return this.peerConnection.setRemoteDescription(answer);
    }
    // 7.用于处理ICE
    onIcecandidate(event) {
        // 如果event.candidate存在,说明有一个新的ICE候选地址产生了
        if (event.candidate) {  
            // 将ICE候选地址, 通常需要通过信令服务器发送给对端
            this.curBroadcas.postMessage({type: 'candidate', candidate: JSON.stringify(event.candidate)});  
        } else {  
            // 如果event.candidate不存在,则表示所有候选地址都已经收集完毕
            // 在某些情况下,这可能意味着ICE候选过程已完成,但并非总是如此
            // 因为在某些情况下,会有多轮ICE候选生成
        }
    }
    // 8.监听轨道赋值给video标签onTrack
    handleOnTrack(event: any) {
        let remoteStream = event.streams[0];
        // 发送订阅消息_给外部
        this.onSubscriptionMsg({event: 'remoteStreams', eventName: '远端视频准备好了', remoteStream})
    }
    
}
兄弟窗口封装
// 其它窗口WebRtc_接听
class AnswerWindowWebRtc {
    // 广播通道
    curBroadcas: BroadcastChannel;
    // webRtc点对点连接
    peerConnection: RTCPeerConnection;
    
    constructor({broadcastChannelName = 'yyh_text'}) {
        this.curBroadcas = CreateBroadcastChannel(broadcastChannelName);
        this.curBroadcas.onmessage = (event) => this.onMessage(event);
        this.handlePageRefreshClose();
    }

    
    // 接收消息
    onMessage(event: any) {
        const msg = event.data;
        // 收到远端SDP
        if (msg.type === 'offer') {
            this.handleCreateNewPerrc();
            this.onSubscriptionMsg({event: 'incomingCall', eventName: '收到新的来电', offer: msg});
        }
        // 保存这些 candidate,candidate 信息主要包括 IP 和端口号,以及所采用的协议类型等
        if (msg.type === 'candidate') {
            const candidate = new RTCIceCandidate(JSON.parse(event.data.candidate));
            this.peerConnection.addIceCandidate(candidate);
        }
        if (msg.type === 'hangup') {
            this.hangup();
        }
    }
    // 发送消息_方法
    postMessage(msg: any) {
        this.curBroadcas.postMessage(msg);
        
    }
    // 收到来电后创建全新的
    handleCreateNewPerrc() {
        // 停止媒体
        this.handleStreamStop();
        // 最好每一次通话都单独创建一个RTCPeerConnection对象,防止复用导致ICE候选的收集受到之前连接的影响,导致画面延迟加载,或其它异常问题无法排查处理;
        this.peerConnection = new RTCPeerConnection();
        this.peerConnection.ontrack = (event) => this.handleOnTrack(event);
        this.peerConnection.oniceconnectionstatechange  = (event) => this.handleOniceconnectionstatechange();
    }
    // 处理页面刷新和关闭方法
    handlePageRefreshClose() {
        window.addEventListener('beforeunload', () => {
            this.postMessage({data: {event: 'otherPageRefresh', eventName: '其它页面刷新了'}});
        });
        window.addEventListener('beforeunload', () => {
            this.postMessage({data: {event: 'otherPageClose', eventName: '其它页面关闭了'}});
        });
    }
    // 处理媒体停止
    handleStreamStop() {
        if (!this.peerConnection) { return; }
        let localStream = this.peerConnection.getSenders();
        localStream.forEach((item: any) => {
            item.track.stop();
        })
    }
    // 卸载
    unmount() {
        if (this.peerConnection) {
            let localStream = this.peerConnection.getSenders();
            localStream.forEach((item: any) => {
                item.track.stop();
            })
            this.peerConnection.close();
            this.peerConnection.onicecandidate = null;
            this.peerConnection.ontrack = null;
            this.peerConnection.oniceconnectionstatechange = null;
            this.peerConnection = null;
        }
        if (this.curBroadcas) {
            this.curBroadcas.onmessage = null;
            this.curBroadcas = null;
        }
    }
    // 挂断
    hangup() {
        if (!this.peerConnection) {
            return;
        }
        if (this.peerConnection.signalingState === 'closed') {
            return;
        };
        this.postMessage({type: 'hangup'});
        // 停止媒体流
        let localStream = this.peerConnection.getSenders();
        localStream.forEach((item: any) => {
            item.track.stop();
        });
        // 关闭peerConection
        this.peerConnection.close();
    }
    // ICE连接状态回调
    handleOniceconnectionstatechange() {
        // 1.检查网络配置
        if (this.peerConnection.iceConnectionState === 'checking') {
            // 发送订阅消息_给外部
            this.onSubscriptionMsg({event: 'iceConnectionState', code: 'checking', eventName: '检查网络配置'});
            // 2.ICE候选者被交换并成功建立了数据传输通道
        } else if (this.peerConnection.iceConnectionState === 'connected') {
            // 发送订阅消息_给外部
            this.onSubscriptionMsg({event: 'iceConnectionState', code: 'connected', eventName: 'ICE候选者被交换并成功建立了数据传输通道'});
            // 3.当连接被关闭或由于某种原因(如网络故障、对端关闭连接等)中断时
        } else if (this.peerConnection.iceConnectionState === 'disconnected') {
            this.hangup();
            this.onSubscriptionMsg({event: 'iceConnectionState', code: 'connected', eventName: 'ICE接被关闭或由于某种原因断开'});
        };
    }
    // 发送订阅消息给外部
    onSubscriptionMsg(msg: {}) {
    }
    // 接听
    answer(msg: any, stream?: MediaStream) {
        return new Promise((resolve, reject) => {
            this.handleStreamAddPeerConnection(stream);
            this.handleSedRemoteSDP(msg).then(() => {
                this.handleCreateAnswer().then((offer) => {
                    this.handleLocalDes(offer).then(() => {
                        this.handleSendAnswerRemoteMsg();
                        resolve({code: 1, message: '已发送接听消息给发送端,等待ice确认'});
                    }).catch(() => {
                        reject({code: 0, message: '本地sdp存储失败'});
                    });
                }).catch(() => {
                    reject({code: 0, message: '创建接听offer失败'});
                });
            }).catch(() => {
                reject({code: 0, message: '远端sdp存储失败'});
            })
        });
    }
    // 1.获取本地媒体流
    getUserMediaToStream(audio: true, video: true) {
        return navigator.mediaDevices.getUserMedia({audio, video});
    }
    // 2.把媒体流轨道添加到 this.peerConnection 中
    handleStreamAddPeerConnection(stream?: MediaStream) {
        if (!stream) {
            stream = new MediaStream();
        }
        const tmpStream = new MediaStream();
        const audioTracks = stream.getAudioTracks();
        const videoTracks = stream.getVideoTracks();
        if (audioTracks.length) {
            tmpStream.addTrack(audioTracks[0]);
            this.peerConnection.addTrack(tmpStream.getAudioTracks()[0], tmpStream);
        }
        if (videoTracks.length) {
            tmpStream.addTrack(videoTracks[0]);
            this.peerConnection.addTrack(tmpStream.getVideoTracks()[0], tmpStream);
        }
    }
    // 3.收到远端邀请_存远端SDP
    handleSedRemoteSDP(msg: any) {
        const answerData = msg;
        const answer = new RTCSessionDescription(answerData);
        return this.peerConnection.setRemoteDescription(answer);
    }
    // 4.接听
    handleCreateAnswer() {
        return this.peerConnection.createAnswer();
    }
    // 5.设置本地SDP描述
    handleLocalDes(offer) {
        return this.peerConnection.setLocalDescription(offer);
    }
    // 6.发送接听消息给远端
    handleSendAnswerRemoteMsg() {
        const answerData = {
            type: this.peerConnection.localDescription.type,
            sdp: this.peerConnection.localDescription.sdp
        };
        // 使用某种方式将answer传递回窗口A
        this.curBroadcas.postMessage(answerData);
    }
    // 7.监听轨道赋值给video标签onTrack
    handleOnTrack(event: any) {
        let remoteStream = event.streams[0];
        // 发送订阅消息_给外部
        this.onSubscriptionMsg({event: 'remoteStreams', eventName: '远端视频准备好了', remoteStream})
    }
}
导出方法
// 创建广播通道_建立两个窗口的广播通道,方便互发消息
function CreateBroadcastChannel(channelName: string) {
    return new BroadcastChannel(channelName);
};
export {CallWindowWebRtc, AnswerWindowWebRtc};
vue3主窗口使用
<template>
    <div class="root">
        <h1>主窗口</h1>
        <div class="loca_right_parent_wrap">
            <div class="loca_video_wrap">
                <div>
                    <button @click="methods.handleVideoToTracks()">发送本地视频给兄弟窗口</button>
                </div>
                <video id="locaVideo" autoplay controls src="./one.mp4" loop width="640" height="480" muted></video>
            </div>
            <div class="remote_video_wrap">
                <div class="tip_text">兄弟窗口视频预览 <button @click="methods.handleHangUp()">挂断</button> </div>
                <video id="remoteVideo" autoplay controls width="640" height="480"></video>
            </div>
        </div>
    </div>
</template>


<script lang="ts">
import {onMounted, onBeforeUnmount} from 'vue';
import {CallWindowWebRtc} from '@/Util/MultiWindowSharingStream';
let curWebRtc: any = null;
export default {
    setup() {
        const methods = {
            handleVideoToTracks() {
                if (curWebRtc) {
                    curWebRtc.unmount && curWebRtc.unmount();
                }
                curWebRtc = new CallWindowWebRtc({});
                // 获取轨道
                const myVideo = document.getElementById('locaVideo');
                const myVideoStream = (myVideo as any).captureStream(30);
                // 呼叫
                curWebRtc.call(myVideoStream);
                
                // 拦截订阅消息
                curWebRtc.onSubscriptionMsg = (msg) => {
                    if (msg.event && msg.event === 'remoteStreams') {
                        const {remoteStream} = msg;
                        const remoteRef = document.getElementById('remoteVideo');
                        (remoteRef as HTMLVideoElement).srcObject = remoteStream;
                        // (remoteRef as HTMLVideoElement).play();
                    }
                }

            },
            handleHangUp() {
                if (curWebRtc) {
                    curWebRtc.hangup && curWebRtc.hangup();
                }
            },
            // 处理组件卸载
            handleUnmount(){
                if (curWebRtc) {
                    curWebRtc.unmount && curWebRtc.unmount();
                }
            }
        }
        onMounted(() => {
           
        });
        onBeforeUnmount(() => {
            methods.handleUnmount();
            
        })
        return {
            methods,
        }
    }
}
</script>

<style lang="scss" scoped>
.root{
    .loca_right_parent_wrap{
        display: flex;
    }
    .loca_video_wrap{
        box-sizing: border-box;
        padding: 0 5px;
        video{
            background: #000;
        }
    }
    .remote_video_wrap{
        box-sizing: border-box;
        padding: 0 5px;
        .tip_text{
            height: 28px;
        }
        video{
            background: #000;
        }
    }
}
</style>
vue3兄弟窗口使用
<template>
    <div class="root">
        <h1>兄弟窗口</h1>
        <div class="loca_right_parent_wrap">
            <div class="loca_video_wrap">
                <div class="tip_text">本地视频预览</div>
                <video id="locaVideo" autoplay controls src="./two.mp4" loop width="640" height="480"></video>
            </div>
            <div class="remote_video_wrap">
                <div class="tip_text">主窗口视频预览 <button @click="methods.handleHangUp()">挂断</button></div>
                <video id="remoteVideo" autoplay controls width="640" height="480"></video>
            </div>
        </div>
    </div>
</template>


<script lang="ts">
import {onMounted, onBeforeUnmount} from 'vue';
import {AnswerWindowWebRtc} from '@/Util/MultiWindowSharingStream';
let curWebRtc: any = null;
export default {
    setup() {
        const methods = {
            handleVideoToTracks() {
            },
            handleInitAnswerOne() {
                if (curWebRtc) {
                    curWebRtc.close && curWebRtc.close();
                    curWebRtc = null;
                }
                curWebRtc = new AnswerWindowWebRtc({});
                const remoteRef = document.getElementById('remoteVideo');
                const myVideo = document.getElementById('locaVideo');
                // 拦截订阅消息
                curWebRtc.onSubscriptionMsg = (msg) => {
                    // 收到远端媒体流
                    if (msg.event && msg.event === 'remoteStreams') {
                        const {remoteStream} = msg;
                        const remoteRef = document.getElementById('remoteVideo');
                        (remoteRef as HTMLVideoElement).srcObject = remoteStream;
                        // (remoteRef as HTMLVideoElement).play();
                    }
                    // 收到新的来电
                    if (msg.event && msg.event === 'incomingCall') {
                        (remoteRef as HTMLVideoElement).srcObject = null;
                        // 获取轨道
                        const locaVideoStream = (myVideo as any).captureStream(30);
                        const {offer} = msg;
                        curWebRtc.answer(offer, locaVideoStream);
                    }
                }

            },
            handleHangUp() {
                if (curWebRtc) {
                    curWebRtc.hangup && curWebRtc.hangup();
                }
            },
            handleUnmount() {
                if (curWebRtc) {
                    curWebRtc.unmount && curWebRtc.unmount();
                    curWebRtc = null;
                }
            }
        }
        onMounted(() => {
            methods.handleInitAnswerOne();
        });
        onBeforeUnmount(() => {
            methods.handleUnmount();
        });
        return {
            methods
        }
    }
}
</script>

<style lang="scss" scoped>
.root{
    .loca_right_parent_wrap{
        display: flex;
    }
    .loca_video_wrap{
        box-sizing: border-box;
        padding: 0 5px;
        .tip_text{
            height: 28px;
        }
        video{
            background: #000;
        }
    }
    .remote_video_wrap{
        box-sizing: border-box;
        padding: 0 5px;
        .tip_text{
            height: 28px;
        }
        video{
            background: #000;
        }
    }
}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/553936.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

分布式搭载博客网站

一.运行环境&#xff1a; IP主机名系统服务192.168.118.128Server-WebLinuxWeb192.168.118.131Server-NFS-DNSLinuxNFS/DNS 二.基础配置 1. 配置主机名&#xff0c;hosts映射 [rootserver ~]# hostnamectl set-hostname Server-Web [rootserver ~]# hostname Server-Web [r…

大模型LLM架构--Decoder-Only、Encoder-Only、Encoder-Decoder

目录 1 LLM演变进化树 2 每种架构的优缺点 2.1 Decoder-Only 架构 2.2 Encoder-Only 2.3 Encoder-Decoder 参考文献&#xff1a; 1 LLM演变进化树 基于 Transformer 模型以非灰色显示&#xff1a;decoder-only 模型在右边的浅蓝色分支&#xff0c;encoder-only 模型在粉色…

【学习】测试新项目该如何高效的展开测试?需要做哪些规划?

当我们收到测试项目时&#xff0c;如何高效地展开测试是我们作为测试人员所要面临的一大挑战。测试是软件开发过程中不可或缺的一环&#xff0c;它确保了产品的质量&#xff0c;降低了出现问题的风险&#xff0c;提高了用户的满意度。因此&#xff0c;我们需要制定合理的测试计…

操作教程丨MaxKB+Ollama:快速构建基于大语言模型的本地知识库问答系统

2024年4月12日&#xff0c;1Panel开源项目组正式对外介绍了其官方出品的开源子项目——MaxKB&#xff08;github.com/1Panel-dev/MaxKB&#xff09;。MaxKB是一款基于LLM&#xff08;Large Language Model&#xff09;大语言模型的知识库问答系统。MaxKB的产品命名内涵为“Max …

java对接IPFS系统-以nft.storage为列

引言 之前我们已经说过了、NFT.Storage是一个基于IPFS的分布式存储服务&#xff0c;专门用于存储和管理非同质化代币&#xff08;NFT&#xff09;相关的数据和资产。它是由Protocol Labs和Pinata共同推出的服务。今天我们基于nft.storage为列、使用java对接打通这个ipfs分布式存…

成为程序员后的领悟与展望-ApiHug

&#x1f917; ApiHug {Postman|Swagger|Api...} 快↑ 准√ 省↓ GitHub - apihug/apihug.com: All abou the Apihug apihug.com: 有爱&#xff0c;有温度&#xff0c;有质量&#xff0c;有信任ApiHug - API design Copilot - IntelliJ IDEs Plugin | Marketplace 选择一个…

【代码】Python3|Requests 库怎么继承 Selenium 的 Headers (2024,Chrome)

本文使用的版本&#xff1a; Chrome 124Python 12Selenium 4.19.0 版本过旧可能会出现问题&#xff0c;但只要别差异太大&#xff0c;就可以看本文&#xff0c;因为本文对新老版本都有讲解。 文章目录 1 难点解析和具体思路2 注意事项2.1 PDF 资源获取时注意事项2.2 Capabiliti…

关于老iPad 能够重新使用经过的一些列折腾

背景 搞了一台IPad air一代给家里老人看戏曲或者电视用,芯片是A7处理器,目前IOS系统是IOS12,也就是能支持的最后一个版本。并且可能是之前刷机问题,IPad基带丢失,显示无法连接激活服务器,无法进入系统。 本人没有MAC设备,没有相关越狱经验,没有黑苹果经验,一切都是从头…

制冷铜管焊接介绍

铜管是制冷装置的重要原材料&#xff0c;它主要有两种用途&#xff1a;①制作换热器。②制作连接管道和管件。常用的焊料类型有铜磷焊料、银铜焊料、铜锌焊料等。在焊接时要根据管道材料的特点&#xff0c;正确的选择焊料及熟练的操作&#xff0c;以确保焊接的质量。 1.1对同类…

基于springboot实现图书进销存管理系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现图书进销存管理系统演示 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了图书进销存管理系统的开发全过程。通过分析图书进销存管理系统管理的不足&#xff0c;创建了一个计算机管理图书进销…

QML QtObject轻量级非可视化元素

QtObject 理论1. 父指针形式代指子类2. 自定义组件中定义一些私有属性 理论 QtObject类型是一个非常轻量级且非可视元素&#xff0c;它只包含objectName属性&#xff0c;其本质上是QObject。 用途一般是两个&#xff1a; 父指针形式代指子类&#xff1b;自定义组件中定义一些…

Java基于SpringBoot+Vue的蜗牛兼职网系统的研究与实现

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

Python零基础从小白打怪升级中~~~~~~~多线程

线程安全和锁 一、全局解释器锁 首先需要明确的一点是GIL并不是Python的特性&#xff0c;它是在实现Python解析器(CPython)时所引入的一个概念。 GIL全称global interpreter lock&#xff0c;全局解释器锁。 每个线程在执行的时候都需要先获取GIL&#xff0c;保证同一时刻只…

MySQL(2024.4.17)

目录 1. 什么是MySQL的MVCC机制&#xff1f; 2. 如何理解InnoDB的Next-Key Lock机制&#xff1f; 3. 快照读和当前读的区别&#xff1f; 4. 如何在SQL语句中触发当前读&#xff1f; 5. MySQL默认的隔离级别是什么&#xff1f; 6. 如何避免在使用当前读时可能出现的死锁问…

Docker部署metahuman-stream数字人系统

metahuman-stream是基于ernerf模型的流式数字人&#xff0c;实现音视频同步对话。 metahuman-stream xtts-streaming-server srs 部署 srs # rtmpserver docker run -it -d \ -p 1935:1935 -p 1985:1985 -p 8080:8080 -p 8000:8000/udp -p 10080:10080/udp \ --name srs \ reg…

Postgresql源码(126)TupleStore使用场景与原理分析

相关 《Postgresql源码&#xff08;125&#xff09;游标恢复执行的原理分析》 《Postgresql游标使用介绍&#xff08;cursor&#xff09;》 总结 开源PG中使用tuple store来缓存tuple集&#xff0c;默认使用work_mem空间存放&#xff0c;超过可以落盘。在PL的returns setof场景…

基于51单片机的步进电机调速系统设计

基于51单片机的步进电机调速系统 &#xff08;仿真&#xff0b;程序&#xff0b;原理图&#xff0b;设计报告&#xff09; 功能介绍 具体功能&#xff1a; 1.按键可以控制电机正、反转&#xff0c;加、减速&#xff0c;停止&#xff1b; 2.一位7段数码管实时显示档位&#xf…

6个免费的伪原创工具,轻松生成原创文章

如今&#xff0c;内容创作已经成为许多人关注的焦点。然而&#xff0c;随之而来的是创作压力和时间成本的增加。为了解决这些问题&#xff0c;越来越多的人开始寻找一些伪原创工具来帮助他们生成原创文章&#xff0c;其中免费的伪原创工具成为了热门选择之一。这些免费的伪原创…

建都寿春的袁术兴亡史

三国(220年-280年)是中国历史上位于汉朝之后&#xff0c;晋朝之前的一段历史时期。这一个时期&#xff0c;先后出现了曹魏、蜀汉、东吴三个主要政权。袁术的地盘很小&#xff0c;为了在三国时期能够立足&#xff1f; 事实上&#xff0c;袁术巅峰时期的地盘并不小&#xff0c;而…

类和对象中-运算符重载

在C中&#xff0c;有些成员函数如果我们不去显示定义&#xff0c;编译器会自动生成 会自动生成的特殊函数&#xff1a; 他们不能定义为全局函数&#xff0c;必须是类成员员函数&#xff08;特别是拷贝赋值重载&#xff09; 下面介绍默认生成函数的作用&#xff0c;特点 构造 …