IOS safari 播放 mp4 遇到的坎儿

news2024/12/28 15:45:45

起因

事情的起因是调试 IOS 手机下播放服务器接口返回的 mp4 文件流失败。对于没调试过移动端和 Safari 的我来说着实费了些功夫,网上和AI也没有讲明白。好在最终大概理清楚了,在这里整理出来供有缘人参考。

问题

因为直接用 IOS 手机的浏览器打开页面去播放也能复现,所以问题出现在 IOS上的 Safari 浏览器上。

问题主要分两类:

  • 一、视频能播放,但是不主动设置 poster,就不显示默认的 poster
  • 二、视频不能播放

首先文件是没有问题的,是主流浏览器都支持的视频编码格式为 H.264(AVC).mp4 文件,由文件格式导致的播放失败问题,网上很多,这里就不赘述了。

问题复现和解决

因为其他浏览器可以正常播放,所以用来对比差异时,我直接在PC端(Edge)查看信息,而 Safari 是在手机上访问项目页面,以及局域网下访问本地运行的 web 服务页面。

用到的工具如下:

  • Live Server 用来运行本地 web 页面
  • node + nodemon + Express 用来运行文件服务,模拟后端接口,nodemon 方便实时同步运行修改内容
  • video.js 播放视频的插件,直接用 CDN。
  • vConsole 移动端查看控制台,直接用 CDN

编写简单 demo

排查问题的时候我是一点一点将实现方式还原到最原始的方式:

  1. 后端接口 + video.js
  2. 后端接口 + video标签
  3. 本地运行的接口 + video标签
  4. 本地文件 + video标签

所以这里再梳理,就可以反向从最简单的方式排查,在本地写个用本地视频文件 + video标签的demo,为了后续排查方便,将事件日志也打印出来。

视频资源我用的 https://vjs.zencdn.net/v/oceans.mp4 下载到本地,不过它的开头是黑屏淡入的,为了方便区分 “不显示poster”,我将开头的几秒黑屏裁剪掉了。
也可以用安卓手机录制一个,默认就是支持播放的 mp4 文件。(PS:别用QQ录屏,编码不对)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
    <script>
      var vConsole = new window.VConsole()
    </script>
    <style>
      #video {
        width: 480px;
        height: 25vh;
        max-width: 100%;
        /* 背景色用于在视频不显示poster时查看占位 */
        background: pink;
        border: 4px solid pink;
      }
    </style>
  </head>
  <body>
    <div class="video-box">
      <video id="video" src="oceans.mp4" controls preload="auto" playsinline="true" muted></video>
    </div>

    <div id="log">
      <h2>日志:方便手机上的 Safari 查看</h2>
      <div class="log__content"></div>
    </div>

    <script>
      // 打印日志
      function log(msg) {
        // 页面上打印日志
        const logContentEl = document.querySelector('.log__content')
        const pEl = document.createElement('p')
        pEl.textContent = msg
        logContentEl.appendChild(pEl)

        // 控制台打印日志
        console.log(msg)
      }

      const video = document.getElementById('video')

      // 主要关心的事件
      const eventNames = [
        { name: 'abort', desc: '当音频/视频的加载已放弃时触发' },
        { name: 'canplay', desc: '当浏览器可以开始播放音频/视频时触发' },
        { name: 'canplaythrough', desc: '当浏览器预计能够在不因缓冲而停顿的情况下持续播放指定的音频/视频时触发' },
        { name: 'durationchange', desc: '当音频/视频的时长已更改时触发' },
        { name: 'error', desc: '当在音频/视频加载期间发生错误时触发' },
        { name: 'loadeddata', desc: '当浏览器已加载音频/视频的当前帧时触发' },
        { name: 'loadedmetadata', desc: '当浏览器已加载音频/视频的元数据时触发' },
        { name: 'loadstart', desc: '当浏览器开始查找音频/视频时触发' },
        { name: 'play', desc: '当音频/视频已开始或不再暂停时触发' },
        { name: 'playing', desc: '当音频/视频在因缓冲而暂停或停止后已就绪时触发' },
        { name: 'progress', desc: '当浏览器正在下载音频/视频时触发' },
        { name: 'timeupdate', desc: '当音频/视频的播放位置发生改变时触发' },
        { name: 'waiting', desc: '当视频由于需要缓冲下一帧而停止,等待时触发' }
      ]

      // 注册video事件监听器
      eventNames.forEach(v => {
        video.addEventListener(v.name, () => {
          // 打印日志
          log(`【readyState: ${video.readyState}${v.name}: ${v.desc}`)
        })
      })
    </script>
  </body>
</html>


preload 默认是 metadata,和 auto 的日志结果一样。为了排除一些可能性,我将其设置了 auto
playsinline="true" 防止 IOS 播放视频时自动打开全屏。

问题1 不显示预览图

demo 页面加载后发现,Safari 浏览器没有显示视频的预览图:

在这里插入图片描述
Edge 显示了预览图:

在这里插入图片描述
通过日志发现,Safari 加载完元数据后(loadedmetadata)就不会继续加载了。

我们知道,视频的预览图就是 video 标签的 poster 属性,当 poster 有值时就会显示指定的图片,当 poster 没有值时,浏览器就会自动处理,而不同浏览器的处理方式也不一样,可能会有这几种情况:

  1. 如果配置了预加载,显示加载后的第一帧,或第二、三桢。
  2. 什么都不显示,或显示默认的播放器背景样式

从日志可以看到,Safari 只加载了元数据,并没有加载画面。查看官方文档,得到了解答:

在这里插入图片描述
再看 poster 的说明:
在这里插入图片描述
既然如此,那只能提供一个 poster 来解决了。

问题2 视频不能播放

问题复现

播放 demo 示例里的视频,是可以正常播放的。

将播放方式改成 video.js + 本地文件,播放是正常的。代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link href="https://unpkg.com/video.js@7.10.2/dist/video-js.min.css" rel="stylesheet" />
    <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
    <script>
      var vConsole = new window.VConsole()
    </script>

    <style>
      #video {
        width: 480px;
        max-width: 100%;
        height: 25vh;
        /* 背景色用于在视频不显示poster时查看占位 */
        background: pink;
        border: 4px solid pink;
      }
    </style>
  </head>
  <body>
    <video id="video" class="video-js" controls preload="auto" playsinline="true" muted>
      <source src="oceans.mp4" type="video/mp4" />
    </video>

    <div id="log">
      <h3>日志:方便手机上的 Safari 查看</h3>
      <div class="log__content"></div>
    </div>

    <script src="https://unpkg.com/video.js@7.10.2/dist/video.min.js"></script>

    <script>
      // 打印日志
      function log(msg) {
        // 页面上打印日志
        const logContentEl = document.querySelector('.log__content')
        const pEl = document.createElement('p')
        pEl.textContent = msg
        logContentEl.appendChild(pEl)

        // 控制台打印日志
        console.log(msg)
      }

      // 主要关心的 videojs 事件
      const eventNames = [
        { name: 'abort', desc: '当音频/视频的加载已放弃时触发' },
        { name: 'canplay', desc: '当浏览器可以开始播放音频/视频时触发' },
        { name: 'canplaythrough', desc: '当浏览器预计能够在不因缓冲而停顿的情况下持续播放指定的音频/视频时触发' },
        { name: 'durationchange', desc: '当音频/视频的时长已更改时触发' },
        { name: 'error', desc: '当在音频/视频加载期间发生错误时触发' },
        { name: 'loadeddata', desc: '当浏览器已加载音频/视频的当前帧时触发' },
        { name: 'loadedmetadata', desc: '当浏览器已加载音频/视频的元数据时触发' },
        { name: 'loadstart', desc: '当浏览器开始查找音频/视频时触发' },
        { name: 'play', desc: '当音频/视频已开始或不再暂停时触发' },
        { name: 'playing', desc: '当音频/视频在因缓冲而暂停或停止后已就绪时触发' },
        { name: 'progress', desc: '当浏览器正在下载音频/视频时触发' },
        { name: 'timeupdate', desc: '当音频/视频的播放位置发生改变时触发' },
        { name: 'waiting', desc: '当视频由于需要缓冲下一帧而停止,等待时触发' }
      ]

      const player = videojs('video')

      player.ready(() => {
        // 注册video事件监听器
        eventNames.forEach(v => {
          player.on(v.name, () => {
            // 打印日志
            log(`【readyState: ${player.readyState()}${v.name}: ${v.desc}`)
          })
        })
      })
    </script>
  </body>
</html>

然后换成后端接口地址,Edge 可以播放,但是 Safari 就失败了。

video 标签方式:
在这里插入图片描述
video.js 方式:报错 The media could not be loaded, either because the server or network failed or because the format is not supported.
在这里插入图片描述

我肯定不是服务器网络故障,也不是文件格式不支持,所以原因只能是接口了。

本地模拟接口

为了不麻烦后端同事,我只能在本地搭建一个服务器,模拟项目接口。

搭建服务

创建 app.js 文件

const express = require('express')
const fs = require('fs')
const path = require('path')

const app = express()

app.get('/', (req, res) => {
  res.send('Stupid IOS')
})

app.listen(3000, () => {
  const ip = 192.169.3.7 // 我的局域网ip
  console.log(`server is running on http://${ip}:3000`)
})

# 安装依赖
npm i express
# 已安装 nodemon,所以直接使用
nodemon app.js

访问 http://192.169.3.7:3000/。把 oceans.mp4 视频文件放到 app.js 同目录下,后面就编写接口就行。

方式1 使用 express 封装好的方法返回文件流
// 方式1: 使用封装好的方法返回文件流
app.get('/type1/:file', (req, res) => {
  const fileName = req.params.file
  const filePath = path.join(__dirname, fileName)

  res.sendFile(filePath)
})

文件地址:http://192.169.3.7:3000/type1/oceans.mp4

video标签和 video.js 播放都正常。

方式2 手动读取全部文件流并返回

同样是接口,本地模拟的正常,项目接口不能播放,那就继续更细致的模拟,手动读取文件流,设置相同的响应头。

查看项目接口的响应头,排除一些范围,只模拟有可能影响的:

// 方式2: 手动读取全部文件流并返回
app.get('/type2/:file', (req, res) => {
  const fileName = req.params.file
  const filePath = path.join(__dirname, fileName)
  
  // 打印请求头
  console.log(req.headers)

  fs.stat(filePath, (err, stats) => {
    if (err) {
      return res.status(404).send('file not found')
    }

    // 设置响应头
    res.set({
      'Accept-Ranges': 'bytes', // 支持 Range 请求
      'Cache-Control': 'no-cache, no-store', // 不缓存
      'Content-Type': 'video/mp4;charset=UTF-8',
      'Content-Range': `bytes 0-${stats.size - 1}/${stats.size}`,
      'Content-Length': stats.size,
      'Content-Disposition': 'attachment;filename="oceans.mp4"',
      ETag: `"${stats.ino.toString()}-${stats.size.toString()}-${Date.now().toString()}"`,
      'Last-Modified': stats.mtime.toUTCString(),
      'Pragma': 'no-cache'
    })

    const stream = fs.createReadStream(filePath)
    stream.pipe(res)
  })
})

文件地址:http://192.169.3.7:3000/type2/oceans.mp4

问题依旧存在,于是查看官方文档,找到一段说明:
在这里插入图片描述

Range 请求就是范围请求或分块传输,客户端通过请求头 Range 指定当前请求想要获取的数据子节范围,服务器根据这个范围,读取文件流,并将读取的内容返回给客户端。
通常,服务器会返回 206 状态码,表示范围请求的响应结果。并且需要在响应头中包含 Content-Range 字段,指明实际返回的数据范围,以及整个资源的总大小。

可是我加了支持 range 请求的响应头啊:'Accept-Ranges': 'bytes'

继续看文档,下面介绍了如何确认服务器是否支持 range 请求:
在这里插入图片描述
大概意思就是主动发送一个指定范围 100 bytes 的请求,看返回的数据是100 bytes,那就是支持,如果返回了整个文件,那就是不支持。

查看之前服务器中打印的请求头,Range 请求头的值:

  • Edge 是 0-,表示获取整个资源
  • Safari 是 0-1:表示获取位置01 的子节的资源,注意可不是从开头到第1个子节,这个范围的请求数是2个子节。

因为我每次都返回的完整的文件流,没有按照 Safari 的要求范围处理,所以属于不支持。

看来仅仅配置响应头是不行的,还要正确处理请求头中的指定范围。

方式3 手动读取指定范围的文件流并返回-分块传输

原来 Safari 会先发送一个获取范围为 bytes=0-1 的请求,以测试服务器是否支持 range 请求。

于是我手动改了下响应头,去掉那些没有影响的,还是返回整个文件流:

    // 设置响应头
    res.set({
      'Accept-Ranges': 'bytes', // 支持 Range 请求
      'Cache-Control': 'no-cache, no-store', // 不缓存
      'Content-Type': 'video/mp4;charset=UTF-8',
      'Content-Range': `bytes 0-1/${stats.size}`,
      'Content-Length': stats.size,
    })

看来仅仅伪造响应头还是不行,还要返回正确大小的文件流,那就编写分块传输的接口:

// 方式3: 手动读取指定范围的文件流并返回-分块传输
app.get('/type3/:file', (req, res) => {
  const fileName = req.params.file
  const filePath = path.join(__dirname, fileName)

  // 查看请求头的范围
  console.log(req.headers.range)

  fs.stat(filePath, (err, stats) => {
    if (err) {
      return res.status(404).send('file not found')
    }

    // 设置响应头
    res.set({
      'Accept-Ranges': 'bytes', // 支持 Range 请求
      'Cache-Control': 'no-cache, no-store', // 不缓存
      'Content-Type': 'video/mp4'
    })

    const range = req.headers.range
    let parts
    let start = 0
    let end = stats.size - 1

    if (range) {
      parts = range.replace(/bytes=/, '').split('-')
      start = parseInt(parts[0], 10)
      end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1
    }

    if (start >= stats.size || end > stats.size) {
      // 如果Range请求超出文件大小,返回416状态码
      return res.status(416).end()
    }

    if (start >= 0 && end >= 0) {
      // 处理 Range 请求
      res.set({
        'Content-Range': `bytes ${start}-${end}/${stats.size}`,
        'Content-Length': end - start + 1
      })
      // 部分内容状态码,返回200浏览器也能正常处理
      res.status(206)
    }

    const stream = fs.createReadStream(filePath, { start, end })
    stream.pipe(res)
  })
})

终于视频可以正常播放了,video.js 也可以播放。

Safari 的校验还挺严格,如果服务器正确处理了这个2子节的请求,Safari 就会开始正式发送正常范围的请求。

服务器的日志可以体现出来:

bytes=0-1
bytes=0-51883958
bytes=196608-51883958
bytes=458752-51883958

最终查看了后端代码,果然接口没有处理 range 请求,在修改逻辑后,功能终于正常。

继续伪造 range 接口

为了搞清楚 Safari 的校验到底有多严格,我再次尝试模拟了一下第一次请求的响应:

	const range = req.headers.range
    let parts
    let start = 0
    let end = stats.size - 1
    
	// 增加逻辑 start-----------------------------------------
    if (range === 'bytes=0-1') {
      // 设置响应头
      res.set({
        'Accept-Ranges': 'bytes', // 支持 Range 请求
        'Cache-Control': 'no-cache, no-store', // 不缓存
        'Content-Type': 'video/mp4;charset=UTF-8',
        'Content-Range': `bytes 3-3/${stats.size}`, // 随便写个范围
        'Content-Length': 2
      })

      const stream = fs.createReadStream(filePath, { start:100, end:200 }) // 随便获取个范围,但不能少于2
      stream.pipe(res)
      return res.status(206) // 随便返回个状态码
    }
	// 增加逻辑 end-----------------------------------------
	
    if (range) {
      parts = range.replace(/bytes=/, '').split('-')
      start = parseInt(parts[0], 10)
      end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1
    }

再次提醒,bytes=0-1 表示请求的位置是01的子节,是2个子节,而不是 1-0=1 的子节数量。

不断测试下,发现只要满足这几个要求,Safari 就认为接口支持 range 请求:

  1. Content-Length 要正确。
  2. Content-Range 的范围要合理。
  3. 返回了不少于 Range 请求头要求大小的文件流数据。

而下面这几点,不影响 Safari 的校验结果:

  1. Content-Range 的范围和 Range 的指定范围不一样
  2. 读取的数据范围和 Range 的指定范围不一样
  3. 返回任意状态码

总结

  1. IOS 上的 Safari 不支持 video 预加载(preload),浏览器不会自动提取帧画面作为默认的 poster 预览图
  2. Safari 上使用 video 播放视频,必须支持并正确处理 Range 范围请求,浏览器会先发送 bytes=0-1 范围的请求来测试服务器是否支持 Range 请求,如果校验成功,就会继续发送正常范围的 Range 请求。否则不再请求资源。

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

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

相关文章

单片机+人体红外感应的防盗系统设计(仿真+源码+PCB文件+报告)

资料下载地址&#xff1a;单片机人体红外感应的防盗系统设计(仿真源码PCB文件报告) 1、功能介绍 (1)该设计包括硬件和软件设计两个部分。 (2)本红外线防盗报警系统由热释电红外传感器、报警器、单片机控制电路、LED控制电路及相关的控制管理软件组成。用户终端完成信息采集、处…

网络攻防实践

1. 学习总结 黛蛇蠕虫案例&#xff1a; 原理&#xff1a;利用系统漏洞&#xff0c;并集成攻击代码。其中&#xff0c;通过蜜罐技术并进行数据分析所获取的攻击场景如下&#xff1a; 外部感染源攻陷蜜罐主机 执行Shellcode后获取主机权限后连接控制命令服务器&#xff0c;获取F…

寒假准备找实习复习java基础-day1

CMD常用命令&#xff1a; java跨平台原理&#xff1a; JRE和JVM Java基本数据类型

MacOS安装Xcode(非App Store)

文章目录 访问官网资源页面 访问官网资源页面 直接访问官网的历史版本下载资源页面地址&#xff1a;https://developer.apple.com/download/more/完成APP ID的登陆&#xff0c;直接找到需要的软件下载即可 解压后&#xff0c;安装将xcode.app移动到应用程序文件夹。

Docker 安装mysql ,redis,nacos

一、Mysql 一、Docker安装Mysql 1、启动Docker 启动&#xff1a;sudo systemctl start dockerservice docker start 停止&#xff1a;systemctl stop docker 重启&#xff1a;systemctl restart docker 2、查询mysql docker search mysql 3、安装mysql 3.1.默认拉取最新版…

gitlab克隆仓库报错fatal: unable to access ‘仓库地址xxxxxxxx‘

首次克隆仓库&#xff0c;失效了&#xff0c;上网查方法&#xff0c;都说是网络代理的问题&#xff0c;各种清理网络代理后都无效&#xff0c;去问同事&#xff1a; 先前都是直接复制的网页url当做远端url&#xff0c;或者点击按钮‘使用http克隆’ 这次对于我来说有效的远端u…

RK356x bsp 7 - PCF8563 RTC调试记录

文章目录 1、环境介绍2、目标3、PCF85634、dts配置5、内核配置6、测试验证 1、环境介绍 硬件&#xff1a;飞凌ok3568-c开发板 软件&#xff1a;原厂rk356x sdk 2、目标 开发板断电后仍正常计时。 3、PCF8563 PCF8563 是由 NXP Semiconductors 公司生产的低功耗 CMOS 实时…

图研院 | 掌握前沿图技术,从 “Graph XAI” 课程起航

在如今数字化转型加速的时代浪潮下&#xff0c; 图数据库技术正成为众多领域突破创新的关键力量&#xff01; 你是否也渴望深入了解其背后的核心知识&#xff0c; 开启自己的图技术进阶之旅&#xff1f; 由机工社精心打造的系列大师课&#xff0c; 特邀国际级专家/学者师资…

Oracle考试多少分算通过?

OCP和OCM认证的考试及格分数并不是固定的&#xff0c;而是根据考试的难度和考生的整体表现来确定。对于OCP认证&#xff0c;考生需要全面掌握考试要求的知识和技能&#xff0c;并在考试中表现出色才有可能通过。而对于OCM认证&#xff0c;考生则需要在每个模块中都达到一定的水…

01.HTTPS的实现原理-HTTPS的概念

01.HTTPS的实现原理-HTTPS的概念 简介1. HTTPS的概念和安全性2. HTTPS的实现原理3. HTTPS和HTTP的区别4. OSI七层协议模型5. SSL和TLS的区别 简介 该系列文章主要讲述了HTTPS协议与HTTP协议的区别&#xff0c;以及HTTPS如何实现安全传输。内容分为三部分&#xff1a;HTTPS的实…

教师管理系统

大概功能&#xff1a; 1.显示所有教师 2.按姓名查找教师 3.按工号查找教师 4.增加教师 5.删除教师 6.退出 数据会保存到 txt 文件里面 姓名&#xff1a;必须是中文 手机号码&#xff1a;必须是11位&#xff0c;必须是数字 效果展示&#xff1a; 代码展示&#xff1a; Teache…

CSES-1687 Company Queries I(倍增法)

题目传送门https://vjudge.net/problem/CSES-1687#authorGPT_zh 解题思路 其实和倍增法求 LCA 是一样的…… 首先设 表示 号点的上面的第 个祖先是谁。 同倍增法&#xff1a; 然后&#xff0c;题目要求我们向上跳 个点。 枚举 &#xff08;从大到小&#xff0c;想想为…

自动驾驶控制算法-横向误差微分方程LQR前馈控制

本文是学习自动驾驶控制算法第六讲 前馈控制与航向误差以及前两节的学习笔记。 1 横向误差微分方程 以规划的轨迹作为自然坐标系&#xff0c;计算自车在轨迹上的投影点&#xff0c;进而计算误差&#xff1a; 如图所示&#xff0c;横向误差为 d d d&#xff0c;航向误差为 θ…

后端开发如何高效使用 Apifox?

Apifox 是一个 API 协作开发平台&#xff0c;后端、前端、测试都可以使用 Apifox 来提升团队的工作效率。对于后端开发者而言&#xff0c;Apifox 的核心功能主要包括四个模块&#xff1a;调用 API、定义 API、开发与调试 API 以及生成 API 文档。本文将详细介绍后端开发人员如何…

【Unity3D】ECS入门学习(六)状态组件 ISystemStateComponentData

当需要获知组件是否被销毁时&#xff0c;ECS是没有回调告知的&#xff0c;因此可以将组件继承于ISystemStateComponentData接口&#xff0c;这样即使组件的实体被销毁了&#xff0c;该组件本身是不会消失的&#xff0c;所以可以通过在组件实体销毁后&#xff0c;去设置状态组件…

LeetCode 19:删除链表的倒数第N 个结点

题目&#xff1a; 地址&#xff1a;https://leetcode.cn/problems/remove-nth-node-from-end-of-list/ 方法一&#xff1a; 方法二&#xff1a; 代码&#xff1a; package com.zy.leetcode.LeetCode_19;/*** Author: zy* Date: 2024-12-25-13:01* Description: 删除链表…

中学数学:一个函数值计算题

在数学的领域中&#xff0c;函数是一种描述变量之间关系的桥梁&#xff0c;它能够揭示出看似复杂现象背后的简洁规律。通过函数&#xff0c;我们可以预测、分析并解决实际问题。在这张图片中&#xff0c;我们看到了一位数学爱好者手写的解题过程&#xff0c;它展示了如何巧妙地…

kipotix4靶机实战

信息收集 1.判断靶机ip 原理&#xff1a;开靶机之前nmap扫一次网段&#xff0c;再开靶机之后扫一次&#xff0c;查看多出来的ip就是靶机ip ip192.168.98.1742.判断端口服务&#xff0c;系统版本 a.确定端口 b.-p指定端口进一步收集 c.信息筛选 1.端口&#xff1a;22,80,139,…

Xilinx FPGA的Bitstream比特流加密设置方法

关于Xilinx FPGA的Bitstream比特流加密设置方法更多信息可参阅应用笔记xapp1084。 使用加密bitstream分两个步骤&#xff1a; 将bitstream的AES密钥存储在FPGA芯片内将使用AES密钥加密的Bitstream通过SPI Flash或JTAG加载至FPGA芯片 AES密钥可以存储在两个存储区之一&#x…

菜鸟带新鸟——基于EPlan2022的部件库制作(3D)

设备逻辑的概念&#xff1a; 可在布局空间 中和其它对象上放置对象。可将其它对象放置在 3D 对象上。已放置的对象分到组件的逻辑结构中。 将此属性的整体标识为设备逻辑。可使用不同的功能创建和编辑设备逻辑。 设备的逻辑定义 定义 / 旋转 / 移动 / 翻转&#xff1a;组…