前言

        H5 语音客服系统可以实现实时的语音通话功能,使客户能够直接与客服人员进行语音交流,通过语音通话,用户可以更直接地表达问题,客服人员也能更准确地理解用户需求,从而更快速地提供解决方案。相比于传统的文字聊天客服系统,语音客服系统更贴近真实的沟通方式,能够更好地理解用户需求和问题,提供更个性化、人性化的服务,提升用户满意度和体验提供更加直接和高效的沟通方式。

        H5 语音客服系统可以在不同的设备和操作系统上运行,包括 PC、手机、平板等,具有良好的跨平台兼容性,能够满足不同用户的需求。

软件定制开发请联系 QQ:993014309或869119955

 HTML 代码

<!DOCTYPE html>

<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
    <meta content="telephone=no" name="format-detection">
    <title>语音通话</title>

    <link href="/h5/css/mui.min.css" rel="stylesheet" />
    <link href="/h5/css/iconfont.css" rel="stylesheet" />
    <link href="/h5/css/app.css" rel="stylesheet" />
    <link href="/h5/css/app-h5.css" rel="stylesheet" />

    <style>
        body {
            width: 100%;
            height: 100%;
            background: url(/h5/img/backgroud.jpg) no-repeat 50%;
            background-size: cover;
            -webkit-overflow-scrolling: touch;
            backdrop-filter: blur(8px);
        }

        #call-status {
            color: #ffffff;
            text-align: center;
            font-size: 1rem;
            margin-top: 3rem;
        }

        #operation {
            width: 100%;
            position: absolute;
            bottom: 3rem;
            text-align: center;
        }

        #call-icon {
            display: inline-block;
            width: 6rem;
            height: 6rem;
            border-radius: 100%;
            background-color: #5de758;
            background-image: url(/h5/img/audio_video_voice.png);
            background-size: 70%;
            background-repeat: no-repeat;
            background-position-x: 50%;
            background-position-y: 50%;
        }

        #call-icon:active {
            background-color: #43b13f;
        }

        #call-text {
            color: #ffffff;
            font-size: 1rem;
        }

        #hangup-icon {
            display: inline-block;
            width: 6rem;
            height: 6rem;
            border-radius: 100%;
            background-color: #e75e58;
            background-image: url(/h5/img/audio_video_hang.png);
            background-size: 70%;
            background-repeat: no-repeat;
            background-position-x: 50%;
            background-position-y: 50%;
        }

        #hangup-icon:active {
            background-color: #b94a45;
        }

        #hangup-text {
            color: #ffffff;
            font-size: 1rem;
        }

        #headpic {
            margin-top: 10rem;
            text-align: center;
        }

        #headpic img {
            width: 8rem;
            height: 8rem;
            border-radius: 100%;
        }

        #name {
            color: #ffffff;
            font-size: 1.6rem;
            text-align: center;
            margin-top: 0.5rem;
        }
</style>
</head>
<body>
    <audio style="display: none" id="localAudio" autoplay></audio>
    <audio style="display: none" id="remoteAudio" autoplay></audio>
    <!-- 消息提醒音频 -->
    <audio id="call-audio" loop>
        <source src="/h5/audio/call.mp3" type="audio/mpeg" />
    </audio>

    <div>
        <div style="position: absolute; width: 100%; top: 0px;">
            <p id="call-status">正在连接服务器</p>
        </div>

        <!-- 显示的头像 -->
        <div id="headpic">
            <img src="/h5/img/cs-all-headpic.png" />
        </div>

        <!-- 显示的名称 -->
        <p id="name">客服热线</p>

        <div id="operation">

            <!-- 拨号 -->
            <div id="call">
                <div id="call-icon"></div>
                <div id="call-text">呼叫</div>
            </div>

            <!-- 取消/挂断 -->
            <div id="hangup" style="display: none;">
                <div id="hangup-icon"></div>
                <div id="hangup-text">取消</div>
            </div>
        </div>
    </div>
</body>
</html>
<script src="/h5/js/jquery-3.4.1.min.js"></script>
<script src="/h5/js/system-1.0.5.js"></script>
<script src="/h5/js/md5.js"></script>
<script src="/h5/js/mui.min.js"></script>
<script src="/h5/js/mui.zoom.js"></script>
<script src="/h5/js/mui.picker.js"></script>
<script src="/h5/js/mui.poppicker.js"></script>
<script src="/h5/js/mui.previewimage.js"></script>
<script src="/h5/js/common.js?v=1.0.8"></script>
<script src="/h5/js/chat.im.js?v=1.0.8"></script>
<script src="/h5/js/adapter-latest.js"></script>
<script src="/h5/js/audio-call.js?v=1.0.8"></script>

<script>

    console.log(adapter.browserDetails.browser);

    init();
</script>

 JavaSprict 代码

// webrtc 连接配置信息
var configuration = {
    "iceServers": [
        {
            // stun 服务器地址、用户名及密码。
            "urls": "stun:xxx.xxx.xxx.xxx:3478",
            "username": "xxx",
            "credential": "xxx"
        },
        {
            // turn 服务器地址、用户名及密码。
            "urls": "turn:xxx.xxx.xxx.xxx:3478",
            "username": "xxx",
            "credential": "xxx"
        }
    ],
    // 默认使用 relay 方式传输数据。
    //"iceTransportPolicy": "relay",
    //"iceCandidatePoolSize": "0"
}

var status = 0;
var statusEnum = {

    normal: 0,
    // 呼叫中。
    call: 1,
    // 人工座席繁忙。
    busySeats: 2,
    // 链接中。
    connecting: 3,
    // 已接通。
    connected: 4
};

var connecting = true;

// 本地音视频流。
var localStream = null;
// 本地音频控件。
var localAudio = document.getElementById("localAudio");

// 远程端音视频流。
var remoteStream = null;
// 远程端音频控件。
var remoteAudio = document.getElementById("remoteAudio");

var callStatus = $("#call-status");
var callAudio = document.getElementById("call-audio");

var peerConnection = null;
var onLocalMediaStreamCall = null;
var onLocalMediaStreamErrorCall = null;

function getAduioCallStatus() {
    return status;
}

/*
 * 离开语音通话房间。
*/
function stopAudioCall() {

    console.log("停止语音通话");
    // 挂断。
    hangup();
    // 关闭本地媒体流。
    closeLocalMedia();
    // 清理倒计时。
    stopCallConnected();
    // 清空远程语音播放。
    remoteAudio.srcObject = null;
}

// 打开音视频设备。
function startAudioCall() {

    console.log("启动语音通话");

    // 检测是否支持音视频设备。
    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {

        $.alert("当前设备不支持语音通话", null, true);
        return false;
    }

    // navigator.getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);

    if (localStream == null) {

        console.log("尝试获取本地音频设备");

        // 获取本地音频设备。
        navigator.mediaDevices.getUserMedia({ audio: true, video: false })
            // 响应成功。
            .then(onLocalMediaStream)
            // 打开音视频设备失败回调函数。
            .catch(function (e) {

                console.error("获取音频设备失败,异常信息:", e);
                $.alert("音频设备访问失败,未授权或发生其他错误", null, true);

                if (onLocalMediaStreamErrorCall) {
                    onLocalMediaStreamErrorCall();
                }

                return;
            });

    } else if (onLocalMediaStreamCall) {
        onLocalMediaStreamCall();
    }

    return true;
}

/*
 * 打开音频设备成功时的回调函数。
*/
function onLocalMediaStream(stream) {

    console.log("获取本地音频设备成功");
    //console.log("onLocalMediaStream", stream);

    // 从设备上获取到的音频 track 添加到 localStream 中。
    if (localStream) {

        console.log("添加新的音频数据流");
        // 包含流中所有的音轨。
        stream.getAudioTracks().forEach(function (track) {

            localStream.addTrack(track);
            stream.removeTrack(track);
        });
    } else {
        localStream = stream;
    }

    // 绑定本地音频控件。
    //localAudio.src = window.URL.createObjectURL(localStream);
    //remoteAudio.srcObject = localStream;

    if (onLocalMediaStreamCall) {

        console.log("回调 onLocalMediaStreamCall");
        onLocalMediaStreamCall();
    }
}

/*
 * 接收远程端音视频流。
*/
function onRemoteStream(e) {

    console.log("收到远程端发送的音视频流");

    remoteStream = e.streams[0];
    // 给音频控件设置数据源。
    //remoteAudio.src = window.URL.createObjectURL(remoteStream);
    remoteAudio.srcObject = remoteStream;
    // 显示已接通的状态。
    showCallStatus(statusEnum.connected);
    // 开始显示通话计时。
    startCallConnected();
}

/*
 * 通过调用 createOffer 生成 Offer SDP 描述符的回调函数。
*/
function onOffer(desc) {

    console.log("onOffer: " + JSON.stringify(desc));

    // 设置本地 SDP。
    peerConnection.setLocalDescription(desc);

    // 将 Offer SDP 发送给远程端。
    sendMessage(chatIm.messageBodyType.offer, JSON.stringify(desc));
}

/*
 * 通过调用 createAnswer 生成 Answer SDP 描述符的回调函数。
*/
function onAnswer(desc) {

    console.log("onAnswer: " + JSON.stringify(desc));

    // 设置本地 SDP。
    peerConnection.setLocalDescription(desc);

    // 将 Answer SDP 发送给远程端。
    sendMessage(chatIm.messageBodyType.answer, JSON.stringify(desc));
}

/*
 * 对方接接受的语音通话邀请。
*/
function receiveAccept() {

    // 停止播放。
    callAudio.pause();
    // 隐藏呼叫按钮。
    $("#call").hide();
    // 显示取消按钮。
    $("#hangup").show();

    // 创建连接。
    createPeerConnection();

    var offerOptions = {
        offerToReceiveAudio: 1,
        offerToReceiveVideo: 0
    };

    // 创建 offer。
    peerConnection.createOffer(offerOptions)
        .then(onOffer)
        .catch(function (e) {
            console.error("Failed to create offer: ", e);
        });
}

/*
 * 挂断。
*/
function hangup() {

    if (peerConnection == null) {
        return;
    }

    peerConnection.close();
    peerConnection = null;
    // 
    console.log("已关闭 peerConnection 对象。");
}

/*
 * 创建 PeerConnection 对象。
*/
function createPeerConnection() {

    showCallStatus(statusEnum.connecting);

    if (peerConnection == null) {

        console.log("创建 peerConnection 对象。");

        peerConnection = new RTCPeerConnection(configuration);
        // 当前收集到 Candidate 后触发的响应事件。
        peerConnection.onicecandidate = function (e) {

            console.log("响应 onicecandidate");

            // 检测 candidate 是否存在。
            if (e.candidate) {
                console.log("candidate: " + e.candidate);
                // 发送给远程端。
                sendMessage(chatIm.messageBodyType.candidate, JSON.stringify(e.candidate));
            }
        }

        // 监听远程端发送的音视频流。
        peerConnection.ontrack = onRemoteStream;
        // 将本地音频轨添加到 PeerConnection 对象中。
        localStream.getTracks().forEach(function (track) {
            peerConnection.addTrack(track, localStream);
            console.log("添加本地音视频流到远程端");
        });

        // 
        console.log("peerConnection 对象初始化完成。");
    }
}


/*
 * 关闭本地媒体流。
*/
function closeLocalMedia() {

    if (localStream == null) {
        return;
    }

    // 遍历每个 track,并将其关闭。
    localStream.getTracks().forEach(function (track) {

        track.stop();
    });

    localStream = null;
    console.log("已关闭 localStream 对象。");
}

/*
 * 通过 webSocket 向远程端发送数据。
 * data: 需要发送的数据对象。
*/
function sendMessage(type, data) {

    console.log("发送消息,type: " + type, data);

    var pack = {
        // 聊天类型。
        bodyType: type,
        // 内容。
        content: data
    };

    // 向服务器发送数据。
    chatIm.send(pack);
}

function onConnect(event) {


}

function onConnectStatus(v) {

    connecting = !v;
    // 显示当前状态。
    showCallStatus(status);
}

/*
 * 响应 webSocket 发送过来的消息。
*/
function onMessage(data) {

    console.log(data);

    if (data.type == chatIm.messageReportType.heartbeat) {
        return;
    }

    if (data.type == chatIm.messageReportType.roomDisband || data.type == chatIm.messageReportType.roomDisband) {

        if (status != statusEnum.normal)
            $.alert("通话意外中断,请重新呼叫", clientHangup);

        return;
    }

    if (data.type != chatIm.messageReportType.receive) {
        return;
    }

    // 获取到消息内容.
    var message = data.value[0];

    switch (message.bodyType) {

        // 有用户接受语音通话邀请了。
        case chatIm.messageBodyType.acceptJoinRoom:
            receiveAccept();
            break;

            // 对方退出了。
        case chatIm.messageBodyType.quitRoom:
            $.alert("对方已挂断");
            hangup();
            hangupStatus();
            break;

            // 远程端发送的 offer sdp。
        case chatIm.messageBodyType.offer:

            if (peerConnection == null) {
                console.error("peerConnection is null.");
                return;
            }

            var json = JSON.parse(message.content);

            console.log("设置远程端发送的 offer", json);

            // 设置媒体协商。
            peerConnection.setRemoteDescription(new RTCSessionDescription(json)).then(function (e) {

                console.log("设置远程端发送的 offer 成功", e);

                // 创建 answer。
                peerConnection.createAnswer().then(onAnswer).catch(function (e) {

                    console.error("Failed to create answer: ", e);
                });
            }).catch(function (e) {
                console.log("设置远程端发送的 offer 失败", e);
            });

            break;

            // 远程端发送的 answer sdp。
        case chatIm.messageBodyType.answer:

            if (peerConnection == null) {
                console.error("peerConnection is null.");
                return;
            }

            var json = JSON.parse(message.content);

            console.log("设置远程端发送的 answer", json);

            // 设置媒体协商。
            peerConnection.setRemoteDescription(new RTCSessionDescription(json)).then(function (e) {

                console.log("设置远程端发送的 answer 成功", e);
            }).catch(function (e) {
                console.log("设置远程端发送的 answer 失败", e);
            });
            break;

            // 收到远程端发送 Candidate 消息。
        case chatIm.messageBodyType.candidate:

            if (peerConnection == null) {
                console.error("peerConnection is null.");
                return;
            }

            var json = JSON.parse(message.content);

            console.log("设置远程端发送的 candidate", json);

            // 将远程端 Candidate 消息添加到 PeerConnection 中。
            peerConnection.addIceCandidate(new RTCIceCandidate(json));
            break;

            // 人工坐席繁忙。
        case chatIm.messageBodyType.busySeats:

            status = statusEnum.busySeats;

            setTimeout(function () {

                if (status == statusEnum.busySeats) {
                    $.alert("人工座席繁忙,请稍候再拨");
                    // 取消。
                    clientHangup();
                }

            }, 3000);
            break;
    }
}

function showCallStatus(v) {

    status = v;

    if (connecting && status == 0) {
        callStatus.text("正在连接服务器");
    } else if (connecting) {
        callStatus.text("当前网络不稳定");
    } else if (status == statusEnum.call) {
        callStatus.text("呼叫中");
    } else if (status == statusEnum.connecting) {
        callStatus.text("正在与对方建立连接");
    }
    else if (status == statusEnum.connected) {
        callStatus.text("已接通");
    }
    else {
        callStatus.text("");
    }
}


function init() {

    // 获取 id、gid。
    var id = system.getCookie("audio-id");
    var gid = system.getCookie("audio-gid");

    // 设置登录状态参数。
    var signInState = ("?chatsa=" + encodeURIComponent(id) + "&chatlt=AYUKLOP&gid=" + encodeURIComponent(gid));

    var host = (urlConfig.chatApiUrl + signInState);

    chatIm.init(host, {

        onConnect: onConnect,
        onConnectStatus: onConnectStatus,
        onMessage: onMessage,

        onMessageSendFail: function (id) {

            console.log(id + " 发送失败");
        }
    });

    onLocalMediaStreamCall = function () {

        // 创建聊天室。
        var pack = {
            // 创建聊天室。
            bodyType: chatIm.messageBodyType.createRoom,
            // 内容。
            content: null
        };

        // 向服务器发送数据。
        chatIm.send(pack);
    }

    onLocalMediaStreamErrorCall = hangupStatus;

    // 给呼叫按钮设置点击事件。
    $("#call").bind("click", clientCall);
    $("#hangup").bind("click", clientHangup);
}

function clientCall() {

    // 启动语音通话。
    if (!startAudioCall()) {
        return;
    }

    callAudio.load();
    // 播放呼叫音频。
    callAudio.play();
    // 隐藏呼叫按钮。
    $("#call").hide();
    // 显示取消按钮。
    $("#hangup").show();

    // 设置为正在呼叫的状态。
    showCallStatus(statusEnum.call);

}

function clientHangup() {

    console.log("主动挂断");

    hangupStatus();
    // 停止语音通话。
    stopAudioCall();
    //
    sendMessage(chatIm.messageBodyType.quitRoom, null);
}

// 取消/挂断状态。
function hangupStatus() {
    callAudio.pause();
    $("#hangup").hide();
    $("#call").show();

    stopCallConnected();
    showCallStatus(statusEnum.normal);
}

var callConnectedTime = 0;
var callConnectedInterval;

// 启动已通话的时间。
function startCallConnected() {

    if (callConnectedInterval != null) {
        clearInterval(callConnectedInterval);
    }

    callConnectedTime = 0;

    callConnectedInterval = setInterval(function () {

        callConnectedTime++;

        callStatus.text(parseInt((callConnectedTime / 60)).toString().padLeft(2, '0') + ":" + (callConnectedTime % 60).toString().padLeft(2, '0'));

    }, 1000);
}

function stopCallConnected() {
    if (callConnectedInterval != null) {
        clearInterval(callConnectedInterval);
    }

    callStatus.text("");
}


总结

        以上就在 H5 中实现语音客服系统的部分代码,需要完整的项目源代码可以在微信公众号中私信我。

微信公众号:KeisoftCN

原文连接:使用 WebRTC 和 H5、JavaSprict 开发一套简单的语音客服系统

Logo

音视频技术社区,一个全球开发者共同探讨、分享、学习音视频技术的平台,加入我们,与全球开发者一起创造更加优秀的音视频产品!

更多推荐