記錄--uniapp開發安卓APP視頻通話模塊初實踐

来源:https://www.cnblogs.com/smileZAZ/archive/2022/11/17/16900022.html
-Advertisement-
Play Games

這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 視頻通話SDK用的即構的,uniapp插件市場地址 推送用的極光的,uniapp插件市場地址 即構音視頻SDK uniapp插件市場的貌似是有些問題,導入不進項目,直接去官網下載,然後放到項目下的 nativeplugins 目錄下,在配 ...


這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助

視頻通話SDK用的即構的,uniapp插件市場地址

推送用的極光的,uniapp插件市場地址

即構音視頻SDK

uniapp插件市場的貌似是有些問題,導入不進項目,直接去官網下載,然後放到項目下的 nativeplugins 目錄下,在配置文件中填入即構後臺的appID和AppSign,接下來就可以開幹了

準備兩個頁面

首頁:/pages/index/index

// 新建一個按鈕
<button @click="sendVideo">發送視頻邀請</button>

// 發送事件,主動發送直接進入下一個頁面即可
sendVideo(){
    uni.navigateTo({
        url: '/pages/call/call'
    })
}

通話頁:pages/call/call 這個頁面會複雜一點

註意這個頁面為 nvue 頁面

先把所有代碼都列出來,再一一做說明

<template>
    <view>
        <view v-if="status === 1" class="switch-bg" :style="{'height': pageH + 'px'}">
            <view class="top-info u-flex" style="flex-direction: row;">
                <image src="http://cdn.u2.huluxia.com/g3/M02/32/81/wKgBOVwN9CiARK1lAAFT4MSyQ3863.jpeg" class="avatar">
                </image>
                <view class="info u-flex u-flex-col u-col-top">
                    <text class="text">值班中心</text>
                    <text class="text" style="margin-top: 10rpx;">正在呼叫</text>
                </view>
            </view>
                <view class="switch-handle u-flex u-row-center" style="flex-direction: row; justify-content: center;">
                    <image src="/static/hang_up.png" class="img" @click="hangUp"></image>
                </view>
        </view>
        <view v-if="status === 2" class="switch-bg" :style="{'height': pageH + 'px'}">
            <view class="top-info u-flex" style="flex-direction: row;">
                <image src="http://cdn.u2.huluxia.com/g3/M02/32/81/wKgBOVwN9CiARK1lAAFT4MSyQ3863.jpeg" class="avatar">
                        </image>
                    <view class="info u-flex u-flex-col u-col-top">
                        <text class="text">值班中心</text>
                        <text class="text" style="margin-top: 10rpx;">邀請您視頻聊天</text>
                    </view>
                </view>
                <view class="switch-handle">
                    <view class="u-flex" style="justify-content: flex-end; flex-direction: row; padding-right: 10rpx; padding-bottom: 30rpx;">
                        <text style="font-size: 26rpx; color: #fff; margin: 10rpx;">切到語音接聽</text>
                        <image src="/static/notice.png" style="width: 64rpx; height: 52rpx;"></image>
                    </view>
                        <view class="u-flex u-row-center u-row-between" style="flex-direction: row; justify-content: space-between;">
                            <image src="/static/hang_up.png" class="img" @click="hangUp"></image>
                            <image src="/static/switch_on.png" class="img" @click="switchOn"></image>
                        </view>
                </view>
        </view>
        <view v-if="status === 3" style="background-color: #232323;" :style="{'height': pageH + 'px'}">
            <view style="flex-direction: row; flex-wrap: wrap;">
                <zego-preview-view class="face" style="width: 375rpx; height: 335rpx;"></zego-preview-view>
                <view v-for="(stream, index) in streamList" :key="index" style="flex-direction: row; flex-wrap: wrap;">
                    <zego-view :streamID="stream.streamID" style="width: 375rpx; height: 335rpx;"></zego-view>
                </view>
                </view>
                <view class="switch-handle">
                        <view style="flex-direction: row; justify-content: center; padding-bottom: 30rpx;">
                                <text style="font-size: 26rpx; color: #fff; margin: 10rpx;">{{minute}}:{{seconds}}</text>
                        </view>
                        <view style="flex-direction: row; justify-content: space-between;">
                                <view style="align-items: center;">
                                        <view class="icon-round">
                                                <image src="/static/notice.png" class="icon1" mode=""></image>
                                        </view>
                                        <text class="h-text">切到語音通話</text>
                                </view>
                                <view style="align-items: center;">
                                        <image src="/static/hang_up.png" class="img" @click="hangUp"></image>
                                        <text class="h-text">掛斷</text>
                                </view>
                                <view style="align-items: center;">
                                        <view class="icon-round" @click="changeCamera">
                                                <image src="/static/change_camera.png" class="icon2" mode=""></image>
                                        </view>
                                        <text class="h-text">轉換攝像頭</text>
                                </view>
                        </view>
                </view>
        </view>
    </view>
</template>

<script>
	// #ifdef APP-PLUS
	var jpushModule = uni.requireNativePlugin("JG-JPush")
	import ZegoExpressEngine from '../../zego-express-video-uniapp/ZegoExpressEngine';
	import {ZegoScenario} from '../../zego-express-video-uniapp/impl/ZegoExpressDefines'
	import {AppID,AppSign} from '../../zegoKey.js'
	var instance = ZegoExpressEngine.createEngine(AppID, AppSign, true, 0);
	// #endif
	export default {
		data() {
			return {
				status: 1, // 1: 主動呼叫;2: 被呼叫
				pageH: '',	// 頁面高度
				innerAudioContext: null, //	音樂對象
				streamList: [],
				msg_id: '',		// 推送消息id
				msg_cid: '',		// 推送cid
				roomID: 'dfmily110001',
				publishStreamID: uni.getStorageSync('userinfo').nickname,
				userID: uni.getStorageSync('userinfo').nickname, 
				userName: uni.getStorageSync('userinfo').nickname, 
				camera_dir: 'before', // 攝像頭 before 前置,after 後置
			};
		},
		destroyed: () => {
			console.log('destroyed');
			ZegoExpressEngine.destroyEngine();
		},
		mounted() {
			var client = uni.getSystemInfoSync()
			if (client.platform == 'android') {
				//安卓事先請求攝像頭、麥克風許可權
				var nativeEngine = uni.requireNativePlugin('zego-ZegoExpressUniAppSDK_ZegoExpressUniAppEngine');
				nativeEngine.requestCameraAndAudioPermission();
			}
		},
		onLoad(opt) {
			this.getSysInfo();
			this.playAudio();
			
			if(opt.status == 2){		// 帶參數 status=2時代表被呼叫
				this.status = parseInt(opt.status)
			}
			if(!opt.status){			// 主動呼叫、需要發推送消息
				this.getPushCid();
			}
			this.initZegoExpress();
		},
		onBackPress() {
			// return true;
			this.innerAudioContext.stop();
			this.logout();
		},
		methods: {
			getSysInfo() { // 獲取手機信息
				let sys = uni.getSystemInfoSync()
				this.pageH = sys.windowHeight
			},
			playAudio() { // 播放音樂
				this.innerAudioContext = uni.createInnerAudioContext();
				this.innerAudioContext.autoplay = true;
				this.innerAudioContext.src = '/static/message.mp3';
				this.innerAudioContext.onPlay(() => {
					console.log('開始播放');
				});
			},
			stopAudio(){		// 停止播放音樂
				if (this.innerAudioContext) {
					this.innerAudioContext.stop()
				}
			},
			hangUp() { // 掛斷
				this.stopAudio();
				this.sendCustomCommand(500)
				this.revocationPushMsg();
				this.logout();
				uni.navigateBack({
					delta:1
				})
			},
			switchOn() { // 接通
				this.stopAudio();
				this.status = 3
				this.sendCustomCommand(200)
			},
			changeCamera() { // 切換攝像頭
				var instance = ZegoExpressEngine.getInstance();
				if (this.camera_dir == 'before') {
					instance.useFrontCamera(false)
					this.camera_dir = 'after'
				} else if (this.camera_dir == 'after') {
					instance.useFrontCamera(true)
					this.camera_dir = 'before'
				}
			},
			sendCustomCommand(msg){		// 發送自定義信令
				var instance = ZegoExpressEngine.getInstance();
				instance.sendCustomCommand(this.roomID, msg, [{
					"userID": this.userID,
					"userName": this.userName
				}], res => {
					console.log(res)
				});
			},
			getPushCid(){			// 極光推送cid獲取
				uni.request({
					url: 'https://api.jpush.cn/v3/push/cid',
					header: {
						'Authorization': 'Basic ' + this.encode(
							'appKey:masterSecret')
					},
					success: (res) => {
						this.msg_cid = res.data.cidlist[0]
						this.sendPushMsg();
					}
				})
			},
			revocationPushMsg(){		// 撤銷推送
				uni.request({
					url: 'https://api.jpush.cn/v3/push/' + this.msg_id,
					method: 'DELETE',
					header: {
						'Authorization': 'Basic ' + this.encode(
							'appKey:masterSecret')
					},
					success: (res) => {
						console.log(res)
					}
				})
			},
			sendPushMsg(idArr) {
				uni.request({
					url: 'https://api.jpush.cn/v3/push',
					method: 'POST',
					header: {
						'Authorization': 'Basic ' + this.encode(
							'appKey:masterSecret')
					},
					data: {
						"cid": this.msg_cid,
						"platform": "all",
						"audience": {
							"registration_id": ['160a3797c8ae473a331']
						},
						"notification": {
							"alert": "邀請通話",
							"android": {},
							"ios": {
								"extras": {
									"newsid": 321
								}
							}
						}
					},
					success: (res) => {
						this.msg_id = res.data.msg_id
					}
				})
			},
			initZegoExpress(){		// 初始化
				// instance.startPreview();
				instance.on('roomStateUpdate', result => {
					console.log('From Native roomStateUpdate:' + JSON.stringify(result));
					if (result['state'] == 0) {
						console.log('房間斷開')
					} else if (result['state'] == 1) {
						console.log('房間連接中')
					} else if (result['state'] == 2) {
						console.log('房間連接成功')
					}
				});
				instance.on('engineStateUpdate', result => {
					if (result == 0) {
						console.log('引擎啟動')
					} else if (result['state'] == 1) {
						console.log('引擎停止')
					}
				});
				instance.on('roomStreamUpdate', result => {
					var updateType = result['updateType'];
					if (updateType === 0) {
						var addedStreamList = result['streamList'];
						this.streamList = this.streamList.concat(addedStreamList);
						for (let i = 0; i < addedStreamList.length; i++) {
							console.log('***********&&&&', addedStreamList[i].streamID)
							var streamID = addedStreamList[i].streamID;
							var instance = ZegoExpressEngine.getInstance();
							instance.startPlayingStream(streamID);
						}
					} else if (updateType === 1) {
						this.removeStreams(result['streamList']);
					}
				});
				instance.on('roomUserUpdate', result => {
					var updateType = result['updateType'];
					if (updateType === 0) {
						this.userID = result.userList[0].userID
						this.userName = result.userList[0].userName
						// this.userList = this.userList.concat(result['userList']);
					} else if (updateType === 1) {
						// this.removeUsers(result['userList']);
					}
				});
				instance.on('IMRecvCustomCommand', result => {
					var fromUser = result['fromUser'];
					var command = result['command'];
					// console.log(`收到${fromUser.userID}的消息:${JSON.stringify(result)}`)
					if(result.command == 200){
						console.log('接聽視頻通話')
						this.status = 3
						this.stopAudio();
					}else if(result.command == 500){
						console.log('拒絕通話')
						uni.navigateBack({
							delta: 1
						})
					}
				});
				this.login();
				this.publish();
			},
			login() {		// 登錄房間
				var instance = ZegoExpressEngine.getInstance();
				instance.loginRoom(this.roomID, {
					'userID': this.userID,
					'userName': this.userName
				});
			},
			logout() {		// 退出房間
				var instance = ZegoExpressEngine.getInstance();
				instance.logoutRoom(this.roomID);
				this.destroyEngine();
			},
			publish() {		// 推流
				var instance = ZegoExpressEngine.getInstance();
				instance.startPublishingStream(this.publishStreamID);
				instance.setVideoConfig({
					encodeWidth: 375,
					encodeHeight: 336
				})
			},
			destroyEngine() {
				ZegoExpressEngine.destroyEngine(boolResult => {
					this.streamList = [];
				});
			},
			removeStreams(removedStreams) {		// 刪除流
				let leg = this.streamList.length
				for (let i = leg - 1; i >= 0; i--) {
					for (let j = 0; j < removedStreams.length; j++) {
						if (this.streamList[i]) {
							if (this.streamList[i].streamID === removedStreams[j].streamID) {
								this.streamList.splice(i, 1)
								continue; //結束當前本輪迴圈,開始新的一輪迴圈
							}
						}
					}
				}
			},
			
			
			
			encode: function(str) {
				// 對字元串進行編碼
				var encode = encodeURI(str);
				// 對編碼的字元串轉化base64
				var base64 = btoa(encode);
				return base64;
			},
		}
	}
</script>

<style lang="scss">
	.switch-bg {
		position: relative;
		background-color: #6B6B6B;
	}

	.top-info {
		padding: 150rpx 35rpx;
		flex-direction: row;
		align-items: center;

		.avatar {
			width: 150rpx;
			height: 150rpx;
			border-radius: 10rpx;
		}

		.info {
			padding-left: 18rpx;

			.text {
				color: #fff;
				font-size: 26rpx;
			}
		}
	}

	.switch-handle {
		position: absolute;
		bottom: 100rpx;
		left: 0;
		right: 0;
		padding: 0 85rpx;

		.img {
			width: 136rpx;
			height: 136rpx;
		}

		.icon-round {
			align-items: center;
			justify-content: center;
			width: 136rpx;
			height: 136rpx;
			border: 1rpx solid #fff;
			border-radius: 50%;

			.icon1 {
				width: 64rpx;
				height: 52rpx;
			}

			.icon2 {
				width: 60rpx;
				height: 60rpx;
			}
		}

		.h-text {
			margin-top: 10rpx;
			font-size: 26rpx;
			color: #fff;
		}
	}
</style>

說明:

代碼中的masterSecret需要修改為極光後臺的masterSecretappKey需要修改為極光後臺的appKey

view 部分:

status=1 中的為主動呼叫方進入頁面是初始顯示內容,最重要的是 hangUp 方法,用來掛斷當前邀請

status=2 中的為被邀請者進入頁面初始顯示的內容,有兩個按鈕,一個hangUp掛斷,一個switchOn 接聽

status=3中為接聽後顯示的內容(顯示自己與對方視頻畫面)

script 部分:

最開始五行是引入相關SDK的。極光推送、即構音視頻

onLoad 中有一個判斷語句,這個就是用於判斷進入頁面時是主動呼叫方還是被動答應方的,顯示不同內容

if(opt.status == 2){		// 帶參數 status=2時代表被呼叫
    this.status = parseInt(opt.status)
}
if(!opt.status){			// 主動呼叫、需要發推送消息
    this.getPushCid();
}

sendCustomCommand 是用來在房間內發送自定義信令的,用於通知另一個人是接聽了還是掛斷了通話

getPushCid 是獲取極光推送的cid,避免重覆發送推送消息(極光推送)

changeCamera 切換攝像頭

revocationPushMsg 撤銷推送(主動呼叫方掛斷通話)

sendPushMsg 發推送消息

initZegoExpress 初始化即構音視頻SDK相關,與官網demo,此處我做了小改動

login 登錄即構房間

logout 退出即構房間

publish 推流

destroyEngine 銷毀音視頻實例

removeStreams 刪除流

encode base64轉碼

在App.vue中進行極光推送的初始化

onLaunch: function() {
    console.log('App Launch')
    // #ifdef APP-PLUS
    if (uni.getSystemInfoSync().platform == "ios") {
        // 請求定位許可權
        let locationServicesEnabled = jpushModule.locationServicesEnabled()
        let locationAuthorizationStatus = jpushModule.getLocationAuthorizationStatus()
        console.log('locationAuthorizationStatus', locationAuthorizationStatus)
        if (locationServicesEnabled == true && locationAuthorizationStatus < 3) {
            jpushModule.requestLocationAuthorization((result) => {
                console.log('定位許可權', result.status)
            })
        }


        jpushModule.requestNotificationAuthorization((result) => {
            let status = result.status
            if (status < 2) {
                uni.showToast({
                    icon: 'none',
                    title: '您還沒有打開通知許可權',
                    duration: 3000
                })
            }
        })

        jpushModule.addGeofenceListener(result => {
            let code = result.code
            let type = result.type
            let geofenceId = result.geofenceId
            let userInfo = result.userInfo
            uni.showToast({
                icon: 'none',
                title: '觸發地理圍欄',
                duration: 3000
            })
        })

        jpushModule.setIsAllowedInMessagePop(true)
        jpushModule.pullInMessage(result => {
            let code = result.code
            console.log(code)
        })

        jpushModule.addInMessageListener(result => {
            let eventType = result.eventType
            let messageType = result.messageType
            let content = result.content
            console.log('inMessageListener', eventType, messageType, content)

            uni.showToast({
                icon: 'none',
                title: JSON.stringify(result),
                duration: 3000
            })
        })

    }

    jpushModule.initJPushService();
    jpushModule.setLoggerEnable(true);
    jpushModule.addConnectEventListener(result => {
        let connectEnable = result.connectEnable
        uni.$emit('connectStatusChange', connectEnable)
    });

    jpushModule.addNotificationListener(result => {
        let notificationEventType = result.notificationEventType
        let messageID = result.messageID
        let title = result.title
        let content = result.content
        let extras = result.extras
        console.log(result)
        this.$util.router(`/pages/public/answer?status=2`)
    });

    jpushModule.addCustomMessageListener(result => {
        let type = result.type
        let messageType = result.messageType
        let content = result.content
        console.log(result)
        uni.showToast({
            icon: 'none',
            title: JSON.stringify(result),
            duration: 3000
        })
    })

    jpushModule.addLocalNotificationListener(result => {
        let messageID = result.messageID
        let title = result.title
        let content = result.content
        let extras = result.extras
        console.log(result)
        uni.showToast({
            icon: 'none',
            title: JSON.stringify(result),
            duration: 3000
        })
    })
    // #endif
},

不要忘了在最開始引入極光推送的插件

var jpushModule = uni.requireNativePlugin("JG-JPush")

官方demo的代碼,直接拿過來了。。

其中最重要的就是下麵這段,用來監聽獲取推送消息的,這裡如果收到推送消息自動跳轉至通話頁面,也就是上面status=2的狀態下

jpushModule.addNotificationListener(result => {
    let notificationEventType = result.notificationEventType
    let messageID = result.messageID
    let title = result.title
    let content = result.content
    let extras = result.extras
    console.log(result)
    this.$util.router(`/pages/call/call?status=2`)
});

https://juejin.cn/post/6954172658195906567

如果對您有所幫助,歡迎您點個關註,我會定時更新技術文檔,大家一起討論學習,一起進步。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • //源文件 void TimerPhyConfig() { RCC->APB1ENR |= (1<<1); //使能Timer3 TIM3->ARR = arr; TIM3->PSC = psc; TIM3->DIER = (1<<0); //Update interrupt enabled TIM ...
  • 本文主要從技術層面探討大數據目前的現狀以及面臨的挑戰。在此之前,如果你對大數據的概念還比較模糊,可閱讀什麼是大數據一文瞭解。 如何定義大數據 目前我們已經瞭解到,大數據是由於數據量的巨大增長而產生的。所以,“大數據”一詞主要描述的是規模巨大的混合數據集,這種數據集是結構化與非結構化數據的融合。 通常 ...
  • 摘要:一文帶你細數幾種ETCD服務異常實例狀態。 本文分享自華為雲社區《【實例狀態】GaussDB ETCD服務異常》,作者:酷哥 。 首先確認是否是虛擬機、網路故障 虛擬機故障導致ETCD服務異常告警 問題現象 管控面上報etcd服務異常告警,虛擬機發生重啟,熱遷移、冷遷移,HA等動作。 問題分析 ...
  • 最近,我們袋鼠雲的UED部⻔小伙伴們,不聲不響地⼲了⼀件⼤事——升級了全新設計語言「數棧UI5.0」。 眾所周知,用戶在使用產品時,是一個動態的過程,用戶和產品之間進行交互的可用性,能否讓用戶愉悅、快速地在產品內達成目的,直接影響用戶使用產品的體驗。 在設計中,有一個廣泛的經驗法則被稱為「尼爾森十大 ...
  • Babelfish for PostgreSQL開源已快一月,不過全網還沒有實踐者總結。今天我們就測試看看,Babelfish到底是如何部署與使用的! ...
  • 現在要是說mysql是什麼東西,就不禮貌了 雖然有的同學沒有進行系統的深入學習,但應該也有個基本概念 【不瞭解也沒關係,後續會進行mysql專欄講解】簡單來說,存儲數據的 學習mysql,就要先安裝它 上官網 : https://dev.mysql.com/downloads/mysql/ 打開網址 ...
  • 11月15日,HMS Core手語服務在2022(第二十一屆)中國互聯網大會 “互聯網助力經濟社會數字化轉型”案例評選活動中,榮獲“特別推薦案例”。 經過一年多的技術迭代和經驗積累,HMS Core手語服務已與多個行業的開發者合作,將AI手語翻譯能力應用在了教育、社交、新聞、政務辦理等場景,助力開發 ...
  • 項目同步git:https://gitee.com/lixin_ajax/vue3-vite-ts-pinia-vant-less.git 覺得有幫助的小伙伴請點下小心心哦 為避免贅述,過於基礎的點會直接省略或貼圖,比如創建文件夾/文件的路徑/路由一類 配置相應功能,也儘量只貼相關代碼,並不代表整個 ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...