局域网TCP通过组播放地址rtp推流和拉流实现实时喊话

应用场景,安卓端局域网不用ip通过组播放地址实现实时对讲功能

发送端: ffmpeg -f alsa -i hw:1 -acodec aac -ab 64k -ac 2 -ar 16000 -frtp -sdp file stream.sdp rtp://224.0.0.1:14556

接收端: ffmpeg -protocol whitelist file,udp,rtp -i stream.sdp -acodec pcm s16le -ar 16000 -ac 2 -f alsa default

在windows上测试通过后然后在安卓中实现

# 查询本地可用麦克风设备
ffmpeg -list_devices true -f dshow -i dummy

麦克风 (Realtek(R) Audio)这是我电脑的
# windows  执行RTM推音频流
ffmpeg -f dshow -i audio="麦克风 (Realtek(R) Audio)" -acodec aac -ab 64k -ac 2 -ar 16000 -f rtp -sdp_file stream.sdp rtp://239.0.0.1:15556

上面windows上调通后接下来在安卓上实现

implementation("com.arthenica:mobile-ffmpeg-full:4.4.LTS")主要用到这个库
package com.xhx.megaphone.tcpimport android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import com.arthenica.mobileffmpeg.Config
import com.arthenica.mobileffmpeg.FFmpeg
import com.blankj.utilcode.util.LogUtils
import com.xhx.megaphone.App
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean/*** 实时推流助手 - 最优化版本* * 功能:* 1. 录音buffer直接实时写入临时文件* 2. FFmpeg同时读取文件进行推流* 3. 最小化延迟的实时推流*/
object LiveStreamingHelper {private const val TAG = "LiveStreamingHelper"// 组播配置private const val MULTICAST_ADDRESS = "239.0.0.1"private const val MULTICAST_PORT = 15556// 音频参数private const val SAMPLE_RATE = 16000private const val CHANNELS = 1private const val BIT_RATE = 64000private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT// 缓冲区大小 - 使用较小的缓冲区减少延迟private val BUFFER_SIZE = AudioRecord.getMinBufferSize(SAMPLE_RATE,AudioFormat.CHANNEL_IN_MONO,AUDIO_FORMAT).let { minSize ->// 使用最小缓冲区的2倍,减少延迟minSize * 2}// 推流状态private val isStreaming = AtomicBoolean(false)private var audioRecord: AudioRecord? = nullprivate var recordingThread: Thread? = nullprivate var ffmpegExecutionId: Long = 0// 文件路径private val cacheDir = File(App.ctx.cacheDir, "live_streaming")private val sdpFile = File(cacheDir, "stream.sdp")private val liveAudioFile = File(cacheDir, "live_audio.pcm")/*** 开始实时录音推流*/fun startStreaming(): Boolean {if (isStreaming.get()) {LogUtils.w(TAG, "推流已在进行中")return false}return try {initializeFiles()createSdpFile()startAudioRecording()startLiveStreaming()isStreaming.set(true)LogUtils.i(TAG, "✅ 实时推流启动成功")true} catch (e: Exception) {LogUtils.e(TAG, "❌ 实时推流启动失败", e)stopStreaming()false}}/*** 停止推流*/fun stopStreaming() {if (!isStreaming.get()) {return}isStreaming.set(false)// 停止录音audioRecord?.stop()audioRecord?.release()audioRecord = null// 停止录音线程recordingThread?.interrupt()recordingThread = null// 停止FFmpegif (ffmpegExecutionId != 0L) {FFmpeg.cancel(ffmpegExecutionId)ffmpegExecutionId = 0}LogUtils.i(TAG, "🛑 实时推流已停止")}/*** 获取推流状态*/fun isStreaming(): Boolean = isStreaming.get()/*** 获取SDP文件路径*/fun getSdpFilePath(): String = sdpFile.absolutePath/*** 获取组播地址信息*/fun getMulticastInfo(): String {val fileSize = if (liveAudioFile.exists()) {"${liveAudioFile.length() / 1024}KB"} else {"0KB"}return "组播地址: $MULTICAST_ADDRESS:$MULTICAST_PORT\n" +"SDP文件: ${sdpFile.absolutePath}\n" +"传输方式: 实时文件流\n" +"缓冲区大小: ${BUFFER_SIZE}字节\n" +"当前文件大小: $fileSize\n" +"推流状态: ${if (isStreaming.get()) "进行中" else "已停止"}"}/*** 初始化文件和目录*/private fun initializeFiles() {if (!cacheDir.exists()) {cacheDir.mkdirs()}// 清理旧文件if (liveAudioFile.exists()) {liveAudioFile.delete()}// 创建新的音频文件liveAudioFile.createNewFile()}/*** 创建SDP文件*/private fun createSdpFile() {val sdpContent = """v=0o=- 0 0 IN IP4 127.0.0.1s=No Namec=IN IP4 $MULTICAST_ADDRESSt=0 0a=tool:libavformat 58.45.100m=audio $MULTICAST_PORT RTP/AVP 97b=AS:64a=rtpmap:97 MPEG4-GENERIC/$SAMPLE_RATE/$CHANNELSa=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; config=141056E500""".trimIndent()sdpFile.writeText(sdpContent)LogUtils.i(TAG, "SDP文件创建成功: ${sdpFile.absolutePath}")}/*** 开始音频录音 - 直接实时写入文件*/private fun startAudioRecording() {audioRecord = AudioRecord(MediaRecorder.AudioSource.MIC,SAMPLE_RATE,AudioFormat.CHANNEL_IN_MONO,AUDIO_FORMAT,BUFFER_SIZE)if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {throw IOException("AudioRecord初始化失败")}audioRecord?.startRecording()// 启动录音线程,实时写入文件recordingThread = Thread {val buffer = ByteArray(BUFFER_SIZE)var fileOutputStream: FileOutputStream? = nullvar totalBytes = 0var lastLogTime = System.currentTimeMillis()try {fileOutputStream = FileOutputStream(liveAudioFile, false) // 不追加,覆盖写入LogUtils.i(TAG, "录音线程启动,实时写入: ${liveAudioFile.absolutePath}")LogUtils.i(TAG, "缓冲区大小: $BUFFER_SIZE 字节")while (isStreaming.get() && !Thread.currentThread().isInterrupted) {val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0if (bytesRead > 0) {// 立即写入文件并刷新fileOutputStream.write(buffer, 0, bytesRead)fileOutputStream.flush()totalBytes += bytesRead// 每3秒打印一次状态(更频繁的状态更新)val currentTime = System.currentTimeMillis()if (currentTime - lastLogTime > 3000) {LogUtils.d(TAG, "🎙️ 实时录音中: ${totalBytes / 1024}KB, 速率: ${(totalBytes / ((currentTime - (lastLogTime - 3000)) / 1000.0) / 1024).toInt()}KB/s")lastLogTime = currentTime}} else if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) {LogUtils.e(TAG, "AudioRecord读取错误: ERROR_INVALID_OPERATION")break} else if (bytesRead < 0) {LogUtils.w(TAG, "AudioRecord读取返回负值: $bytesRead")}}LogUtils.i(TAG, "录音线程结束,总计: ${totalBytes / 1024}KB")} catch (e: Exception) {LogUtils.e(TAG, "录音数据写入异常", e)} finally {fileOutputStream?.close()}}recordingThread?.start()LogUtils.i(TAG, "音频录音已启动")}/*** 启动实时推流*/private fun startLiveStreaming() {GlobalScope.launch(Dispatchers.IO) {// 等待一些音频数据写入Thread.sleep(300)// 构建FFmpeg命令 - 使用较小的缓冲区和实时参数val command = "-re -f s16le -ar $SAMPLE_RATE -ac $CHANNELS " +"-thread_queue_size 512 " +  // 增加线程队列大小"-i ${liveAudioFile.absolutePath} " +"-acodec aac -ab ${BIT_RATE/1000}k -ac $CHANNELS -ar $SAMPLE_RATE " +"-f rtp -sdp_file ${sdpFile.absolutePath} " +"rtp://$MULTICAST_ADDRESS:$MULTICAST_PORT"LogUtils.i(TAG, "FFmpeg实时推流命令: $command")ffmpegExecutionId = FFmpeg.executeAsync(command) { executionId, returnCode ->LogUtils.i(TAG, "FFmpeg推流结束: executionId=$executionId, returnCode=$returnCode")when (returnCode) {Config.RETURN_CODE_SUCCESS -> {LogUtils.i(TAG, "✅ 推流正常结束")}Config.RETURN_CODE_CANCEL -> {LogUtils.i(TAG, "🛑 推流被用户取消")}else -> {LogUtils.w(TAG, "⚠️ 推流异常结束,返回码: $returnCode")}}}LogUtils.i(TAG, "FFmpeg执行ID: $ffmpegExecutionId")}}
}

拉流

package com.xhx.megaphone.tcpimport android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioTrack
import com.arthenica.mobileffmpeg.Config
import com.arthenica.mobileffmpeg.FFmpeg
import com.blankj.utilcode.util.LogUtils
import com.xhx.megaphone.App
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.File
import java.io.RandomAccessFile
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong/*** 低延迟拉流播放助手* * 优化策略:* 1. 最小化FFmpeg缓冲* 2. 减少AudioTrack缓冲区* 3. 更频繁的数据读取* 4. 优化文件IO*/
object LowLatencyPullHelper {private const val TAG = "LowLatencyPullHelper"// 音频参数private const val SAMPLE_RATE = 16000private const val CHANNELS = 1private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT// 低延迟参数private const val SMALL_BUFFER_SIZE = 1024  // 使用更小的缓冲区private const val READ_INTERVAL_MS = 20     // 更频繁的读取间隔// 拉流状态private val isPulling = AtomicBoolean(false)private var ffmpegExecutionId: Long = 0private var audioTrack: AudioTrack? = nullprivate var playbackThread: Thread? = null// 文件读取位置private val fileReadPosition = AtomicLong(0)// 文件路径private val cacheDir = File(App.ctx.cacheDir, "low_latency_pull")private val outputPcmFile = File(cacheDir, "realtime_audio.pcm")/*** 开始低延迟拉流播放*/fun startPulling(sdpFilePath: String): Boolean {if (isPulling.get()) {LogUtils.w(TAG, "拉流已在进行中")return false}val sdpFile = File(sdpFilePath)if (!sdpFile.exists()) {LogUtils.e(TAG, "SDP文件不存在: $sdpFilePath")return false}return try {initializeFiles()startLowLatencyDecoding(sdpFilePath)startLowLatencyPlayback()isPulling.set(true)fileReadPosition.set(0)LogUtils.i(TAG, "✅ 低延迟拉流播放启动成功")true} catch (e: Exception) {LogUtils.e(TAG, "❌ 低延迟拉流播放启动失败", e)stopPulling()false}}/*** 停止拉流*/fun stopPulling() {if (!isPulling.get()) {return}isPulling.set(false)// 停止FFmpegif (ffmpegExecutionId != 0L) {FFmpeg.cancel(ffmpegExecutionId)ffmpegExecutionId = 0}// 停止音频播放audioTrack?.stop()audioTrack?.release()audioTrack = null// 停止播放线程playbackThread?.interrupt()playbackThread = nullLogUtils.i(TAG, "🛑 低延迟拉流已停止")}/*** 获取拉流状态*/fun isPulling(): Boolean = isPulling.get()/*** 获取拉流信息*/fun getPullInfo(): String {val fileSize = if (outputPcmFile.exists()) {"${outputPcmFile.length() / 1024}KB"} else {"0KB"}return "拉流状态: ${if (isPulling.get()) "进行中" else "已停止"}\n" +"解码文件: ${outputPcmFile.absolutePath}\n" +"文件大小: $fileSize\n" +"读取位置: ${fileReadPosition.get() / 1024}KB\n" +"优化模式: 低延迟"}/*** 初始化文件和目录*/private fun initializeFiles() {if (!cacheDir.exists()) {cacheDir.mkdirs()}// 清理旧文件if (outputPcmFile.exists()) {outputPcmFile.delete()}}/*** 启动低延迟FFmpeg解码*/private fun startLowLatencyDecoding(sdpFilePath: String) {GlobalScope.launch(Dispatchers.IO) {// 减少等待时间Thread.sleep(500)// 构建超低延迟FFmpeg解码命令val command = "-protocol_whitelist file,udp,rtp " +"-fflags +nobuffer+flush_packets " +      // 禁用缓冲并立即刷新"-flags low_delay " +                     // 低延迟模式"-probesize 32 " +                        // 最小探测大小"-analyzeduration 0 " +                   // 不分析流"-max_delay 0 " +                         // 最大延迟为0"-reorder_queue_size 0 " +                // 禁用重排序队列"-rw_timeout 3000000 " +                  // 3秒超时"-i $sdpFilePath " +"-acodec pcm_s16le " +"-ar $SAMPLE_RATE " +"-ac $CHANNELS " +"-f s16le " +"-flush_packets 1 " +                     // 立即刷新数据包"${outputPcmFile.absolutePath}"LogUtils.i(TAG, "低延迟FFmpeg解码命令: $command")ffmpegExecutionId = FFmpeg.executeAsync(command) { executionId, returnCode ->LogUtils.i(TAG, "FFmpeg解码结束: executionId=$executionId, returnCode=$returnCode")when (returnCode) {Config.RETURN_CODE_SUCCESS -> {LogUtils.i(TAG, "✅ 解码正常结束")}Config.RETURN_CODE_CANCEL -> {LogUtils.i(TAG, "🛑 解码被用户取消")}else -> {LogUtils.w(TAG, "⚠️ 解码异常结束,返回码: $returnCode")}}}}}/*** 启动低延迟音频播放*/private fun startLowLatencyPlayback() {// 使用最小缓冲区val minBufferSize = AudioTrack.getMinBufferSize(SAMPLE_RATE,AudioFormat.CHANNEL_OUT_MONO,AUDIO_FORMAT)// 使用稍大于最小缓冲区的大小,但不要太大val bufferSize = minBufferSize * 2audioTrack = AudioTrack(AudioManager.STREAM_MUSIC,SAMPLE_RATE,AudioFormat.CHANNEL_OUT_MONO,AUDIO_FORMAT,bufferSize,AudioTrack.MODE_STREAM)// 设置低延迟模式(API 26+)try {if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {val audioAttributes = android.media.AudioAttributes.Builder().setUsage(android.media.AudioAttributes.USAGE_MEDIA).setContentType(android.media.AudioAttributes.CONTENT_TYPE_MUSIC).setFlags(android.media.AudioAttributes.FLAG_LOW_LATENCY).build()audioTrack = AudioTrack.Builder().setAudioAttributes(audioAttributes).setAudioFormat(AudioFormat.Builder().setEncoding(AUDIO_FORMAT).setSampleRate(SAMPLE_RATE).setChannelMask(AudioFormat.CHANNEL_OUT_MONO).build()).setBufferSizeInBytes(bufferSize).setTransferMode(AudioTrack.MODE_STREAM).build()}} catch (e: Exception) {LogUtils.w(TAG, "无法设置低延迟AudioTrack,使用默认配置", e)}audioTrack?.play()LogUtils.i(TAG, "AudioTrack初始化完成,缓冲区大小: $bufferSize")// 启动高频率播放线程playbackThread = Thread {val buffer = ByteArray(SMALL_BUFFER_SIZE)var totalPlayed = 0var lastLogTime = System.currentTimeMillis()LogUtils.i(TAG, "低延迟音频播放线程启动")while (isPulling.get() && !Thread.currentThread().isInterrupted) {try {if (outputPcmFile.exists()) {val currentFileSize = outputPcmFile.length()val currentReadPos = fileReadPosition.get()// 如果有新数据可读if (currentFileSize > currentReadPos) {RandomAccessFile(outputPcmFile, "r").use { randomAccessFile ->randomAccessFile.seek(currentReadPos)val bytesRead = randomAccessFile.read(buffer)if (bytesRead > 0) {audioTrack?.write(buffer, 0, bytesRead)totalPlayed += bytesReadfileReadPosition.addAndGet(bytesRead.toLong())// 每2秒打印一次状态val currentTime = System.currentTimeMillis()if (currentTime - lastLogTime > 2000) {LogUtils.d(TAG, "🔊 低延迟播放: ${totalPlayed / 1024}KB, 延迟: ${(currentFileSize - currentReadPos) / 32}ms")lastLogTime = currentTime}}}}}// 高频率检查,减少延迟Thread.sleep(READ_INTERVAL_MS.toLong())} catch (e: Exception) {LogUtils.e(TAG, "低延迟播放异常", e)Thread.sleep(100)}}LogUtils.i(TAG, "低延迟播放线程结束,总计播放: ${totalPlayed / 1024}KB")}playbackThread?.start()}
}

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

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

相关文章

基于深度学习的医学图像分析:使用YOLOv5实现细胞检测

最近研学过程中发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击链接跳转到网站人工智能及编程语言学习教程。读者们可以通过里面的文章详细了解一下人工智能及其编程等教程和学习方法。下面开始对正文内容的…

32.768KHZ 3215晶振CM315D与NX3215SA应用全场景

在现代电子设备中&#xff0c;一粒米大小的晶振&#xff0c;却是掌控时间精度的“心脏”。CITIZEN的CM315D系列与NDK的NX3215SA系列晶振便是其中的佼佼者&#xff0c;它们以 3.2 1.5 mm 的小尺寸”(厚度不足1mm)&#xff0c;成为智能设备中隐形的节奏大师。精准计时的奥秘这两…

嵌软面试——ARM Cortex-M寄存器组

Cortex-M内存架构包含16个通用寄存器&#xff0c;其中R0-R12是13个32位的通用寄存器&#xff0c;另外三个寄存器是特殊用途&#xff0c;分别是R13&#xff08;栈指针&#xff09;,R14&#xff08;链接寄存器&#xff09;,R15&#xff08;程序计数器&#xff09;。对于处理器来说…

7.DRF 过滤、排序、分页

过滤Filtering 对于列表数据可能需要根据字段进行过滤&#xff0c;我们可以通过添加django-fitlter扩展来增强支持。 pip install django-filter在配置文件中增加过滤器类的全局设置&#xff1a; """drf配置信息必须全部写在REST_FRAMEWORK配置项中""…

二、CUDA、Pytorch与依赖的工具包

CUDA Compute Unified Device Architecture&#xff08;统一计算架构&#xff09;。专门用于 GPU 通用计算 的平台 编程接口。CUDA可以使你的程序&#xff08;比如矩阵、神经网络&#xff09;由 GPU 执行&#xff0c;这比CPU能快几十甚至上百倍。 PyTorch 是一个深度学习框架…

SpringCloude快速入门

近期简单了解一下SpringCloude微服务,本文主要就是我学习中所记录的笔记,然后具体原理可能等以后再来深究,本文可能有些地方用词不专业还望包容一下,感兴趣可以参考官方文档来深入学习一下微服务,然后我的下一步学习就是docker和linux了。 nacos: Nacos 快速开始 | Nacos 官网…

GPT Agent与Comet AI Aent浏览器对比横评

1. 架构设计差异GPT Agent的双浏览器架构&#xff1a;文本浏览器&#xff1a;专门用于高效处理大量文本内容&#xff0c;适合深度信息检索和文献追踪&#xff0c;相当于Deep Research的延续可视化浏览器&#xff1a;具备界面识别与交互能力&#xff0c;可以点击网页按钮、识别图…

应用信息更新至1.18.0

增加DO权限 增加权限管理&#xff08;需DO支持&#xff09; 增加应用冻结隐藏&#xff08;需DO支持&#xff09; 增加权限委托&#xff08;需DO支持&#xff09; 增加特殊组件 ...

常用git命令集锦

git init 初始化 将当前目录初始化为 git 本地仓库&#xff0c;此时会在本地创建一个 .git 的文件夹 git init -q 静默执行&#xff0c;就是在后台执行 git init --bare –bare 参数,一般用来初始化一个空的目录&#xff0c;作为远程存储仓库 git init --template dir –templa…

skywalking安装

一、简介 SkyWalking是一款用于分布式系统跟踪和性能监控的开源工具。它可以帮助开发人员了解分布式系统中不同组件之间的调用关系和性能指标&#xff0c;从而进行故障排查和性能优化。 它支持多种语言和框架&#xff0c;包括Java、.NET、Node.js等。它通过在应用程序中插入代…

利用DataStream和TrafficPeak实现大数据可观察性

可观察性工作流对于深入了解应用程序的健康状况、客户流量和整体性能至关重要。然而&#xff0c;要实现真正的可观察性还面临一些挑战&#xff0c;包括海量的流量数据、数据保留、实施时间以及各项成本等。TrafficPeak是一款为Akamai云平台打造&#xff0c;简单易用、可快速部署…

jQuery 最新语法大全详解(2025版)

引言 jQuery 作为轻量级 JavaScript 库&#xff0c;核心价值在于 简化 DOM 操作、跨浏览器兼容性和高效开发。尽管现代框架崛起&#xff0c;jQuery 仍在遗留系统维护、快速原型开发中广泛应用。本文涵盖 jQuery 3.6 核心语法&#xff0c;重点解析高效用法与最佳实践。 一、jQu…

Android 15 修改截图默认音量大小

概述 在 Android 15 中,截图音效的默认音量可能过大,影响用户体验。本文将介绍如何通过修改系统源码来调整截图音效的默认音量大小。 修改位置 需要修改的文件路径: frameworks/base/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundProvider.kt…

Python爬虫实战:快速采集教育政策数据(附官网工具库API)

解锁教育政策研究的数据金矿&#xff0c;用技术提升学术效率 在教育政策研究领域&#xff0c;获取最新、最全面的政策文本是学术工作的基础。传统手动收集方式效率低下且容易遗漏关键政策&#xff0c;而Python爬虫技术为教育研究者提供了高效的数据采集解决方案。本文将系统介…

验证回文串-leetcode

如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后&#xff0c;短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。 字母和数字都属于字母数字字符。 给你一个字符串 s&#xff0c;如果它是 回文串 &#xff0c;返回 true &#xff1b;否则&#xf…

嵌入式学习日志(十)

10 学习指针1 指针核心定义与本质1.1 指针与指针变量1、指针即地址&#xff0c;指针变量是存放地址的变量&#xff0c;其大小与操作系统位数相关&#xff1a;64 位系统中占 8 字节&#xff0c;32 位系统中占 4 字节。2、指针的核心功能是通过地址间接访问目标变量&#xff0…

Anaconda创建环境报错:CondaHTTPEFTOT: HTTP 403 FORBIDDEN for url

一、快速解决方案这类报错的原因通常是由于 conda 无法访问镜像源或权限被服务器拒绝&#xff0c;以下是常见原因和对应的解决方案&#xff1a;检查镜像源拼写是否正确conda config --show channels清华源镜像示例如果不正确&#xff0c;先清除旧配置del %USERPROFILE%\.condar…

亚马逊地址关联暴雷:新算法下的账号安全保卫战

2025年Q3&#xff0c;上千个店铺因共享税代地址、海外仓信息重叠等问题被批量冻结&#xff0c;为行业敲响了“精细化合规”的警钟。事件复盘&#xff1a;地址成为关联风控的“致命开关”税代机构违规引发“多米诺效应”事件的导火索指向税代机构“saqibil”&#xff0c;其为降低…

在本地环境中运行 ‘dom-distiller‘ GitHub 库的完整指南

在本地环境中运行 ‘dom-distiller’ GitHub 库的完整指南 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家&#xff0c;觉得好请收藏。点击跳转到网站。 1. 项目概述 ‘dom-distiller’ 是一个用于将网页…

11. isaacsim4.2教程-Transform 树与Odometry

1. 前言学习目标在本示例中&#xff0c;你将学习如何&#xff1a;使用 TF 发布器将相机作为 TF 树的一部分发布在 TF 上发布机械臂&#xff0f;可动结构&#xff08;articulation&#xff09;的树状结构发布里程计&#xff08;Odometry&#xff09;消息开始之前前置条件已完成 …