基于Linphone android sdk开发Android软话机

news2025/2/1 19:51:40

1.Linphone简介

1.1 简介

LinPhone是一个遵循GPL协议的开源网络电话或者IP语音电话(VOIP)系统,其主要如下。使用linphone,开发者可以在互联网上随意的通信,包括语音、视频、即时文本消息。linphone使用SIP协议,是一个标准的开源网络电话系统,能将linphone与任何基于SIP的VoIP运营商连接起来,包括我们自己开发的免费的基于SIP的Audio/Video服务器。

LinPhone是一款自由软件(或者开源软件),你可以随意的下载和在LinPhone的基础上二次开发。LinPhone是可用于Linux, Windows, MacOSX 桌面电脑以及Android, iPhone, Blackberry移动设备。

学习LinPhone的源码,开源从以下几个部分着手: Java层框架实现的SIP三层协议架构: 传输层,事务层,语法编解码层; linphone动态库C源码实现的SIP功能: 注册,请求,请求超时,邀请会话,挂断电话,邀请视频,收发短信... linphone动态库C源码实现的音视频编解码功能; Android平台上的音视频捕获,播放功能;

1.2 基本使用

如果是Android系统用户,可以从谷歌应用商店安装或者从这个链接下载Linphone 。安装完成后,点击左上角的菜单按钮,选择进入助手界面。在助手界面,可以设定SIP账户或者Linphone账号,如下图:图片来自网路

 

2.基于linphone android sdk开发linphone

  • 引入sdk依赖 

dependencies {
    //linphone
    debugImplementation "org.linphone:linphone-sdk-android-debug:5.0.0"
    releaseImplementation "org.linphone:linphone-sdk-android:5.0.0"

为了方便调用,我们需要对Linphone进行简单的封装。首先,按照官方文档的介绍,创建一个CoreManager类,此类是sdk里面的管理类,用来控制来电铃声和启动CoreService,无特殊需求不需调用。需要注意的是,启动来电铃声需要导入media包,否则不会有来电铃声,如下

implementation 'androidx.media:media:1.2.0'
  • 基本代码开发 
package com.matt.linphonelibrary.core

import android.annotation.SuppressLint
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.telephony.PhoneStateListener
import android.telephony.TelephonyManager
import android.util.Log
import android.view.TextureView
import com.matt.linphonelibrary.R
import com.matt.linphonelibrary.callback.PhoneCallback
import com.matt.linphonelibrary.callback.RegistrationCallback
import com.matt.linphonelibrary.utils.AudioRouteUtils
import com.matt.linphonelibrary.utils.LinphoneUtils
import com.matt.linphonelibrary.utils.VideoZoomHelper
import org.linphone.core.*
import java.io.File
import java.util.*


class LinphoneManager private constructor(private val context: Context) {
    private val TAG = javaClass.simpleName

    private var core: Core
    private var corePreferences: CorePreferences
    private var coreIsStart = false
    var registrationCallback: RegistrationCallback? = null
    var phoneCallback: PhoneCallback? = null


    init {
        //日志收集
        Factory.instance().setLogCollectionPath(context.filesDir.absolutePath)
        Factory.instance().enableLogCollection(LogCollectionState.Enabled)

        corePreferences = CorePreferences(context)
        corePreferences.copyAssetsFromPackage()
        val config = Factory.instance().createConfigWithFactory(
            corePreferences.configPath,
            corePreferences.factoryConfigPath
        )
        corePreferences.config = config

        val appName = context.getString(R.string.app_name)
        Factory.instance().setDebugMode(corePreferences.debugLogs, appName)

        core = Factory.instance().createCoreWithConfig(config, context)
    }

    private var previousCallState = Call.State.Idle

    private val coreListener = object : CoreListenerStub() {
        override fun onGlobalStateChanged(core: Core, state: GlobalState?, message: String) {
            if (state === GlobalState.On) {
            }
        }

        //登录状态回调
        override fun onRegistrationStateChanged(
            core: Core,
            cfg: ProxyConfig,
            state: RegistrationState,
            message: String
        ) {
            when (state) {
                RegistrationState.None -> registrationCallback?.registrationNone()
                RegistrationState.Progress -> registrationCallback?.registrationProgress()
                RegistrationState.Ok -> registrationCallback?.registrationOk()
                RegistrationState.Cleared -> registrationCallback?.registrationCleared()
                RegistrationState.Failed -> registrationCallback?.registrationFailed()
            }
        }

        //电话状态回调
        override fun onCallStateChanged(
            core: Core,
            call: Call,
            state: Call.State,
            message: String
        ) {
            Log.i(TAG, "[Context] Call state changed [$state]")

            when (state) {
                Call.State.IncomingReceived, Call.State.IncomingEarlyMedia -> {
                    if (gsmCallActive) {
                        Log.w(
                            TAG,
                            "[Context] Refusing the call with reason busy because a GSM call is active"
                        )
                        call.decline(Reason.Busy)
                        return
                    }

                    phoneCallback?.incomingCall(call)
                    gsmCallActive = true

                    //自动接听
                    if (corePreferences.autoAnswerEnabled) {
                        val autoAnswerDelay = corePreferences.autoAnswerDelay
                        if (autoAnswerDelay == 0) {
                            Log.w(TAG, "[Context] Auto answering call immediately")
                            answerCall(call)
                        } else {
                            Log.i(
                                TAG,
                                "[Context] Scheduling auto answering in $autoAnswerDelay milliseconds"
                            )
                            val mainThreadHandler = Handler(Looper.getMainLooper())
                            mainThreadHandler.postDelayed({
                                Log.w(TAG, "[Context] Auto answering call")
                                answerCall(call)
                            }, autoAnswerDelay.toLong())
                        }
                    }
                }

                Call.State.OutgoingInit -> {
                    phoneCallback?.outgoingInit(call)
                    gsmCallActive = true
                }

                Call.State.OutgoingProgress -> {
                    if (core.callsNb == 1 && corePreferences.routeAudioToBluetoothIfAvailable) {
                        AudioRouteUtils.routeAudioToBluetooth(core, call)
                    }
                }

                Call.State.Connected -> phoneCallback?.callConnected(call)

                Call.State.StreamsRunning -> {
                    // Do not automatically route audio to bluetooth after first call
                    if (core.callsNb == 1) {
                        // Only try to route bluetooth / headphone / headset when the call is in StreamsRunning for the first time
                        if (previousCallState == Call.State.Connected) {
                            Log.i(
                                TAG,
                                "[Context] First call going into StreamsRunning state for the first time, trying to route audio to headset or bluetooth if available"
                            )
                            if (AudioRouteUtils.isHeadsetAudioRouteAvailable(core)) {
                                AudioRouteUtils.routeAudioToHeadset(core, call)
                            } else if (corePreferences.routeAudioToBluetoothIfAvailable && AudioRouteUtils.isBluetoothAudioRouteAvailable(
                                    core
                                )
                            ) {
                                AudioRouteUtils.routeAudioToBluetooth(core, call)
                            }
                        }
                    }

                    if (corePreferences.routeAudioToSpeakerWhenVideoIsEnabled && call.currentParams.videoEnabled()) {
                        // Do not turn speaker on when video is enabled if headset or bluetooth is used
                        if (!AudioRouteUtils.isHeadsetAudioRouteAvailable(core) &&
                            !AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed(core, call)
                        ) {
                            Log.i(
                                TAG,
                                "[Context] Video enabled and no wired headset not bluetooth in use, routing audio to speaker"
                            )
                            AudioRouteUtils.routeAudioToSpeaker(core, call)
                        }
                    }
                }
                Call.State.End, Call.State.Released, Call.State.Error -> {
                    if (core.callsNb == 0) {
                        when (state) {
                            Call.State.End -> phoneCallback?.callEnd(call)

                            Call.State.Released -> phoneCallback?.callReleased(call)

                            Call.State.Error -> {
                                val id = when (call.errorInfo.reason) {
                                    Reason.Busy -> R.string.call_error_user_busy
                                    Reason.IOError -> R.string.call_error_io_error
                                    Reason.NotAcceptable -> R.string.call_error_incompatible_media_params
                                    Reason.NotFound -> R.string.call_error_user_not_found
                                    Reason.Forbidden -> R.string.call_error_forbidden
                                    else -> R.string.call_error_unknown
                                }
                                phoneCallback?.error(context.getString(id))
                            }
                        }
                        gsmCallActive = false
                    }
                }
            }
            previousCallState = state
        }
    }

    /**
     * 启动linphone
     */
    fun start() {
        if (!coreIsStart) {
            coreIsStart = true
            Log.i(TAG, "[Context] Starting")
            core.addListener(coreListener)
            core.start()

            initLinphone()

            val telephonyManager =
                context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
            Log.i(TAG, "[Context] Registering phone state listener")
            telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
        }
    }

    /**
     * 停止linphone
     */
    fun stop() {
        coreIsStart = false
        val telephonyManager =
            context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager

        Log.i(TAG, "[Context] Unregistering phone state listener")
        telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE)

        core.removeListener(coreListener)
        core.stop()
    }


    /**
     * 注册到服务器
     *
     * @param username     账号名
     * @param password      密码
     * @param domain     IP地址:端口号
     */
    fun createProxyConfig(
        username: String,
        password: String,
        domain: String,
        type: TransportType? = TransportType.Udp
    ) {
        core.clearProxyConfig()

        val accountCreator = core.createAccountCreator(corePreferences.xmlRpcServerUrl)
        accountCreator.language = Locale.getDefault().language
        accountCreator.reset()

        accountCreator.username = username
        accountCreator.password = password
        accountCreator.domain = domain
        accountCreator.displayName = username
        accountCreator.transport = type

        accountCreator.createProxyConfig()
    }


    /**
     * 取消注册
     */
    fun removeInvalidProxyConfig() {
        core.clearProxyConfig()

    }


    /**
     * 拨打电话
     * @param to String
     * @param isVideoCall Boolean
     */
    fun startCall(to: String, isVideoCall: Boolean) {
        try {
            val addressToCall = core.interpretUrl(to)
            addressToCall?.displayName = to
            val params = core.createCallParams(null)
            //启用通话录音
//            params?.recordFile = LinphoneUtils.getRecordingFilePathForAddress(context, addressToCall!!)
            //启动低宽带模式
            if (LinphoneUtils.checkIfNetworkHasLowBandwidth(context)) {
                Log.w(TAG, "[Context] Enabling low bandwidth mode!")
                params?.enableLowBandwidth(true)
            }
            if (isVideoCall) {
                params?.enableVideo(true)
                core.enableVideoCapture(true)
                core.enableVideoDisplay(true)
            } else {
                params?.enableVideo(false)
            }
            if (params != null) {
                core.inviteAddressWithParams(addressToCall!!, params)
            } else {
                core.inviteAddress(addressToCall!!)
            }

        } catch (e: Exception) {
            e.printStackTrace()
        }

    }


    /**
     * 接听来电
     *
     */
    fun answerCall(call: Call) {
        Log.i(TAG, "[Context] Answering call $call")
        val params = core.createCallParams(call)
        //启用通话录音
//        params?.recordFile = LinphoneUtils.getRecordingFilePathForAddress(context, call.remoteAddress)
        if (LinphoneUtils.checkIfNetworkHasLowBandwidth(context)) {
            Log.w(TAG, "[Context] Enabling low bandwidth mode!")
            params?.enableLowBandwidth(true)
        }
        params?.enableVideo(isVideoCall(call))
        call.acceptWithParams(params)
    }

    /**
     * 谢绝电话
     * @param call Call
     */
    fun declineCall(call: Call) {
        val voiceMailUri = corePreferences.voiceMailUri
        if (voiceMailUri != null && corePreferences.redirectDeclinedCallToVoiceMail) {
            val voiceMailAddress = core.interpretUrl(voiceMailUri)
            if (voiceMailAddress != null) {
                Log.i(TAG, "[Context] Redirecting call $call to voice mail URI: $voiceMailUri")
                call.redirectTo(voiceMailAddress)
            }
        } else {
            Log.i(TAG, "[Context] Declining call $call")
            call.decline(Reason.Declined)
        }
    }

    /**
     * 挂断电话
     */
    fun terminateCall(call: Call) {
        Log.i(TAG, "[Context] Terminating call $call")
        call.terminate()
    }

    fun micEnabled() = core.micEnabled()

    fun speakerEnabled() = core.outputAudioDevice?.type == AudioDevice.Type.Speaker

    /**
     * 启动麦克风
     * @param micEnabled Boolean
     */
    fun enableMic(micEnabled: Boolean) {
        core.enableMic(micEnabled)
    }

    /**
     * 扬声器或听筒
     * @param SpeakerEnabled Boolean
     */
    fun enableSpeaker(SpeakerEnabled: Boolean) {
        if (SpeakerEnabled) {
            AudioRouteUtils.routeAudioToEarpiece(core)
        } else {
            AudioRouteUtils.routeAudioToSpeaker(core)
        }
    }


    /**
     * 是否是视频电话
     * @return Boolean
     */
    fun isVideoCall(call: Call): Boolean {
        val remoteParams = call.remoteParams
        return remoteParams != null && remoteParams.videoEnabled()
    }


    /**
     * 设置视频界面
     * @param videoRendering TextureView 对方界面
     * @param videoPreview CaptureTextureView 自己界面
     */
    fun setVideoWindowId(videoRendering: TextureView, videoPreview: TextureView) {
        core.nativeVideoWindowId = videoRendering
        core.nativePreviewWindowId = videoPreview
    }

    /**
     * 设置视频电话可缩放
     * @param context Context
     * @param videoRendering TextureView
     */
    fun setVideoZoom(context: Context, videoRendering: TextureView) {
        VideoZoomHelper(context, videoRendering, core)
    }

    fun switchCamera() {
        val currentDevice = core.videoDevice
        Log.i(TAG, "[Context] Current camera device is $currentDevice")

        for (camera in core.videoDevicesList) {
            if (camera != currentDevice && camera != "StaticImage: Static picture") {
                Log.i(TAG, "[Context] New camera device will be $camera")
                core.videoDevice = camera
                break
            }
        }

//        val conference = core.conference
//        if (conference == null || !conference.isIn) {
//            val call = core.currentCall
//            if (call == null) {
//                Log.w(TAG, "[Context] Switching camera while not in call")
//                return
//            }
//            call.update(null)
//        }
    }


    //初始化一些操作
    private fun initLinphone() {

        configureCore()

        initUserCertificates()
    }


    private fun configureCore() {
        // 来电铃声
        core.isNativeRingingEnabled = false
        // 来电振动
        core.isVibrationOnIncomingCallEnabled = true
        core.enableEchoCancellation(true) //回声消除
        core.enableAdaptiveRateControl(true) //自适应码率控制

    }

    private var gsmCallActive = false
    private val phoneStateListener = object : PhoneStateListener() {
        override fun onCallStateChanged(state: Int, phoneNumber: String?) {
            gsmCallActive = when (state) {
                TelephonyManager.CALL_STATE_OFFHOOK -> {
                    Log.i(TAG, "[Context] Phone state is off hook")
                    true
                }
                TelephonyManager.CALL_STATE_RINGING -> {
                    Log.i(TAG, "[Context] Phone state is ringing")
                    true
                }
                TelephonyManager.CALL_STATE_IDLE -> {
                    Log.i(TAG, "[Context] Phone state is idle")
                    false
                }
                else -> {
                    Log.i(TAG, "[Context] Phone state is unexpected: $state")
                    false
                }
            }
        }
    }


    //设置存放用户x509证书的目录路径
    private fun initUserCertificates() {
        val userCertsPath = corePreferences!!.userCertificatesPath
        val f = File(userCertsPath)
        if (!f.exists()) {
            if (!f.mkdir()) {
                Log.e(TAG, "[Context] $userCertsPath can't be created.")
            }
        }
        core.userCertificatesPath = userCertsPath
    }


    companion object {

        // For Singleton instantiation
        @SuppressLint("StaticFieldLeak")
        @Volatile
        private var instance: LinphoneManager? = null
        fun getInstance(context: Context) =
            instance ?: synchronized(this) {
                instance ?: LinphoneManager(context).also { instance = it }
            }

    }

}

3.封装好的源码 

网上已经有对linphone android sdk开发好的产品

LinphoneCall封装linphone android sdk的软话机

4.优化的配置

对于部分设备可能存在啸叫、噪音的问题,可以修改assets/linphone_factory 文件下的语音参数,默认已经配置了一些,如果不能满足你的要求,可以添加下面的一些参数。

回声消除
  • echocancellation=1:回声消除这个必须=1,否则会听到自己说话的声音
  • ec_tail_len= 100:尾长表示回声时长,越长需要cpu处理能力越强
  • ec_delay=0:延时,表示回声从话筒到扬声器时间,默认不写
  • ec_framesize=128:采样数,肯定是刚好一个采样周期最好,默认不写
回声抑制
  • echolimiter=0:等于0时不开会有空洞的声音,建议不开
  • el_type=mic:这个选full 和 mic 表示抑制哪个设备
  • eq_location=hp:这个表示均衡器用在哪个设备
  • speaker_agc_enabled=0:这个表示是否启用扬声器增益
  • el_thres=0.001:系统响应的阈值 意思在哪个阈值以上系统有响应处理
  • el_force=600 :控制收音范围 值越大收音越广,意思能否收到很远的背景音
  • el_sustain=50:控制发声到沉默时间,用于控制声音是否拉长,意思说完一个字是否被拉长丢包时希望拉长避免断断续续
降噪
  • noisegate=1 :这个表示开启降噪音,不开会有背景音
  • ng_thres=0.03:这个表示声音这个阈值以上都可以通过,用于判断哪些是噪音
  • ng_floorgain=0.03:这个表示低于阈值的声音进行增益,用于补偿声音太小被吃掉
网络抖动延时丢包
  • audio_jitt_comp=160:这个参数用于抖动处理,值越大处理抖动越好,但声音延时较大 理论值是80根据实际调整160
  • nortp_timeout=20:这个参数用于丢包处理,值越小丢包越快声音不会断很长时间,同时要跟el_sustain配合声音才好听

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2024年你的年度目标OKR制定好了吗?

标题2023年余额见底,2024年的FLAG都制定好了吗? 目标很明确,计划很丰满,执行起来又处处透着一点点乏力,怎么办? 2024年可以尝试用OKR制定目标。 OKR目标管理方法,既适用于企业,也…

连续色调图像的二维编码

连续色调图像的二维编码(也被称为连续色调图像的矢量量化)是一种图像压缩和数据储存的方法,适用于连续渐变的色彩信息。该编码方法有助于减小图像文件的体积,并被广泛应用于数字图像处理、存储和传输领域。本文将介绍连续色调图像…

【科学计算语言】实验四 科学计算与可视化

【目的和要求】 (1)理解科学计算实质并掌握Python语言的科学计算应用 (2)掌握常用科学计算库 (3)熟练运用numpy及scipy、matplotlib等计算库资源 【实验准备】 Python核心科学计算库的导入、配置并熟悉相关…

Hive-high Avaliabl

hive—high Avaliable ​ hive的搭建方式有三种,分别是 ​ 1、Local/Embedded Metastore Database (Derby) ​ 2、Remote Metastore Database ​ 3、Remote Metastore Server ​ 一般情况下,我们在学习的时候直接使用hive –service metastore的方式…

基于ChatGLM搭建专业领域问答机器人的思路

如果我们对ChatGLM进一步提出涉及专业领域的问题,而此方面知识是ChatGLM未经数据训练的,那么ChatGLM的回答效果如何呢?本节将考察ChatGLM在专业领域的问答水平,并尝试解决此方面的问题。 在使用ChatGLM制作专业领域问答机器人之前…

【Amazon 实验①】Amazon WAF功能增强之实验环境准备

文章目录 1. 实验介绍2. 实验环境准备 1. 实验介绍 在真实的网络空间中,攻击者会使用大量广泛分布的僵尸网络、肉机等发起对目标的攻击。 其来源分布一般比较分散,因此难以简单防范。 本实验联合使用有多种AWS服务:Cloudfront、 Lambdaedge…

服务器数据恢复-服务器断电导致linux操作系统数据丢失的数据恢复案例

linux操作系统服务器数据恢复环境: 某品牌R730服务器MD3200系列存储,linux操作系统。 服务器故障: 机房意外断电导致服务器linux操作系统部分文件丢失。 服务器数据恢复过程: 1、将故障服务器连接到北亚企安数据恢复中心备份服务器…

【开源工程及源码】超级经典开源项目实景三维数字孪生智慧港口

智慧港口可视化平台,旨在实现对港口运营的全面监测、智能管理和优化决策。飞渡科技利用数字化、模拟和仿真的技术,通过互联的传感器和设备,实现实时数据的采集、传输和分析,将港口内外的复杂数据以直观、易懂的方式呈现&#xff0…

前端学习——vuex的入门

学习一门技术最快捷的方式就是先了解其概念和使用场景,毕竟任何技术的出现都是为了解决某一个场景下的通用解决方案,并且使用最合理的方式去解决问题。 那么什么是vuex? Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 库。它采用集中…

Python---搭建Python自带静态Web服务器

1. 静态Web服务器是什么? 可以为发出请求的浏览器提供静态文档的程序。 平时我们浏览百度新闻数据的时候,每天的新闻数据都会发生变化,那访问的这个页面就是动态的,而我们开发的是静态的,页面的数据不会发生变化。 …

使用 Postman 进行并发请求:实用教程与最佳实践

背景介绍 最近,我们发起了一个在线图书管理系统的项目。我负责的一个关键模块包括三个主要后台接口: 实现对books数据的检索。实施对likes数据的获取。通过collections端点访问数据。 应对高流量的挑战 在设计并部署接口时,我们不可避免地…

tomcat和nginx自定义404错误页面

nginx 编辑nginx配置文件 vim /www/server/nginx/nginx.conf server{listen 80;error_page 404 /404.html;location /404.html{root /home/liu/html/error-html;} }在家目录下创建一个html/error-html目录,用于存放错误页面 在error-html目录下创建404.html&a…

心有暖阳,笃定前行,2024考研加油

2024考研学子,所有的付出终有收获,阳光终将穿透阴霾,终将上岸。 当曙光破晓的时候,你可曾记得那些星月为伴,孤独为友,理想为灯来指引前行之路的日子,那些默默扎根的日子终将化作星星在未来闪闪发…

第一部分 数理逻辑

目录 什么是命题 注意: 例1 下列句子中那些是命题? 联结词 例2 将下列命题符号化. 注意: 例4 设 p:天冷,q:小王穿羽绒服,将下列命题符号化 例5 求下列复合命题的真值 例如 真值表: 例&#xff1…

我的应用我做主:扩展线程池

自定义线程创建:ThreadFactory 线程池中的线程是从哪里来的呢? ThreadPoolExecutor(int corePoolSize,//指定了线程池种的线程数量 int maximumPoolSize,//指定了线程池中的最大线程数量。 long keepAliveTime,// 当线程池数量超过了corePoolSize&#x…

MyBatis关联查询(二、一对多查询)

MyBatis关联查询(二、一对多查询) 需求:查询所有用户信息及用户关联的账户信息。 分析:用户信息和他的账户信息为一对多关系,并且查询过程中如果用户没有账户信息,此时也要将用户信息查询出来&#xff0c…

Python (十四)pandas(二)

程序员的公众号:源1024,获取更多资料,无加密无套路! 最近整理了一波电子书籍资料,包含《Effective Java中文版 第2版》《深入JAVA虚拟机》,《重构改善既有代码设计》,《MySQL高性能-第3版》&…

WPF组合控件TreeView+DataGrid之DataGrid封装-粉丝专栏

wpf的功能非常强大,很多控件都是原生的,但是要使用TreeViewDataGrid的组合,就需要我们自己去封装实现。 我们需要的效果如图所示: 这2个图都是第三方控件自带的,并且都是收费使用。 现在我们就用原生的控件进行封装一…

rouyi-vue-pro+vue3+vite4+Element Plus项目中使用生成Vue2+Element UI标准模板

运行一个pro-vue3的前端项目&#xff0c;以及后端服务 在基础设施-代码生成模块中选择某张数据库表导入&#xff0c;并编辑生成信息&#xff0c;前端类型&#xff1a;Vue2Element UI标准模板 在vue3项目中创建一个vue文件 <template> </template><script>…

云轴科技ZStack 英特尔联合成立云系统解决方案创新中心

近日&#xff0c;云轴科技ZStack 与英特尔联合成立云系统解决方案创新中心&#xff0c;并完成揭牌仪式。双方将基于多年合作基础进一步加快研发下一代创新技术和解决方案&#xff0c;为客户和合作伙伴提供更高效、安全的云基础设施解决方案。 左三:云轴科技ZStack 联合创始人兼…