首页
关于
壁纸
直播
留言
友链
统计
Search
1
《三国志英杰传》攻略
6,152 阅读
2
Emby客户端IOS破解
6,019 阅读
3
白嫖Emby
6,004 阅读
4
《吞食天地1》金手指代码
5,602 阅读
5
破解emby-server
4,299 阅读
moonjerx
game
age-of-empires
zx3
san-guo-zhi
尼尔:机械纪元
net
emby
learn-video
docker
torrent
photoshop
route
minio
git
ffmpeg
im
vue
gitlab
typecho
svn
alipay
nasm
srs
mail-server
tailscale
kkfileview
aria2
webdav
synology
redis
oray
chemical
mxsite
math
π
x-ui
digital-currency
server
nginx
baota
k8s
http
cloud
linux
shell
database
vpn
esxi
rancher
domain
k3s
ewomail
os
android
windows
ios
app-store
macos
develop
java
javascript
uniapp
nodejs
hbuildx
maven
android-studio
jetbrain
jenkins
css
mybatis
php
python
hardware
hard-disk
pc
RAM
software
pt
calibre
notion
office
language
literature
philosophy
travel
登录
Search
标签搜索
ubuntu
mysql
openwrt
zerotier
springboot
centos
openvpn
jdk
吞食天地2
synology
spring
idea
windows11
吞食天地1
transmission
google-play
Japanese
xcode
群晖
kiftd
MoonjerX
累计撰写
378
篇文章
累计收到
465
条评论
首页
栏目
moonjerx
game
age-of-empires
zx3
san-guo-zhi
尼尔:机械纪元
net
emby
learn-video
docker
torrent
photoshop
route
minio
git
ffmpeg
im
vue
gitlab
typecho
svn
alipay
nasm
srs
mail-server
tailscale
kkfileview
aria2
webdav
synology
redis
oray
chemical
mxsite
math
π
x-ui
digital-currency
server
nginx
baota
k8s
http
cloud
linux
shell
database
vpn
esxi
rancher
domain
k3s
ewomail
os
android
windows
ios
app-store
macos
develop
java
javascript
uniapp
nodejs
hbuildx
maven
android-studio
jetbrain
jenkins
css
mybatis
php
python
hardware
hard-disk
pc
RAM
software
pt
calibre
notion
office
language
literature
philosophy
travel
页面
关于
壁纸
直播
留言
友链
统计
搜索到
69
篇与
develop
的结果
2022-12-13
docker搭建maven私有仓库
(一)引言在实际开发工作中,通常需要搭建maven私有仓库。(二)Nexus介绍 Nexus 是Maven仓库管理器,如果你使用Maven,你可以从Maven中央仓库 下载所需要的构件(artifact),但这通常不是一个好的做法,你应该在本地架设一个Maven仓库服务器,在代理远程仓库的同时维护本地仓库,以节省带宽和时间,Nexus就可以满足这样的需要。此外,他还提供了强大的仓库管理功能,构件搜索功能,它基于REST,友好的UI是一个extjs的REST客户端,它占用较少的内存,基于简单文件系统而非数据库。这些优点使其日趋成为最流行的Maven仓库管理器。(三)安装docker请参考 Docker安装_Icoolkj的博客(四)docker中安装nexus31、拉取镜像## 通过docker search nexus 命令搜索一下docker公有库在的 nexus相关的镜像 [root@icoolkj ~]# docker search nexus## 拉取nexus3镜像 [root@icoolkj /]# docker pull sonatype/nexus3 Using default tag: latest latest: Pulling from sonatype/nexus3 f70d60810c69: Pull complete 545277d80005: Pull complete 10b49635409a: Pull complete Digest: sha256:3fd7e90bcf49fb55d87d852cab854e5669ed115b09bdb25f47c45ee0797231aa 147.6MB/295.3MB Status: Downloaded newer image for sonatype/nexus3:latest docker.io/sonatype/nexus3:latest [root@icoolkj /]# 2、建立数据储存文件夹## 建立数据存放文件夹,用于docker中nexus的数据与本地物理机映射 [root@icoolkj /]# mkdir -p /usr/local/nexus3/nexus-data [root@icoolkj /]# ll /usr/local/nexus3/ 总用量 4 drwxr-xr-x 2 root root 4096 6月 3 18:06 nexus-data ## 更改权限 [root@icoolkj /]# chmod 777 /usr/local/nexus3/nexus-data/ [root@icoolkj /]# ll /usr/local/nexus3/ 总用量 4 drwxr-xr-x 2 777 root 4096 6月 3 18:06 nexus-data [root@icoolkj /]# 3、安装并运行容器## 编写启动脚本docker-nexus3-start.sh docker rm -f docker-nexus3 || true docker run --name docker-nexus3 \ -p 8081:8081 \ -v /usr/local/nexus3/nexus-data:/nexus-data \ --restart=always \ -d sonatype/nexus3 ## 参数说明 --name nexus #启动该容器的名字,可以自己更改为自己想要的名字 -p 8081:8081 #端口映射,将本地8081端口映射为容器的8081端口,第一个8081可以改成自己想要放开的端口 -v /docker/nexus/nexus-data:/nexus-data # 将容器的/nexus-data地址 代理到 本地/docker/nexus/nexus-data文件夹下 --restart=always #在重启docker时,自动重启改容器。 -d sonatype/nexus3 #即为后台运行一直sonatype/nexus34、获取容器的日志[root@icoolkj nexus3]# docker logs -f --tail 20 docker-nexus3(五)使用nexus31、浏览器访问2、配置Nexus提示输入密码,并告知你的密码储存位置Your admin user password is located in /nexus-data/admin.password on the server.因为docker中nexus3的数据储存位置与本地物理机建立了映射关系,所有在物理机上的地址应该是/usr/local/nexus3/nexus-data/admin.password登录成功后需要更改密码,更改密码需要记住(浏览器都有记住密码的功能,顺⼿点保存⾯,下次你直接登录就好了);更改密码完成之后,admin.password⽂件⾃动删除!!!## 默认仓库说明 maven-central:maven中央库,默认从https://repo1.maven.org/maven2/拉取jar maven-releases:私库发行版jar,初次安装请将Deployment policy设置为Allow redeploy maven-snapshots:私库快照(调试版本)jar maven-public:仓库分组,把上面三个仓库组合在一起对外提供服务,在本地maven基础配置settings.xml或项目pom.xml中使用## Nexus仓库类型介绍 hosted:本地仓库,通常我们会部署自己的构件到这一类型的仓库。比如公司的第二方库。 proxy:代理仓库,它们被用来代理远程的公共仓库,如maven中央仓库。 group:仓库组,用来合并多个hosted/proxy仓库,当你的项目希望在多个repository使用资源时就不需要多次引用了,只需要引用一个group即可。如图所示,代理仓库负责代理远程中央仓库,托管仓库负责本地资源,组资源库 = 代理资源库 + 托管资源库3、配置阿里云代理仓库1)、新建仓库(Create repository)Repository-->Repositories-->Create repository-->maven2(proxy)填写仓库名称——maven-aliyun,并填入仓库urlhttps://maven.aliyun.com/repository/public2)、配置仓库组(默认已有一个maven-public) 注:注意仓库顺序。maven查找依赖时会依次遍历仓库组中的仓库。## 官方文档中建议: It is recommended practice to place hosted repositories higher in the list than proxy repositories. For proxy repositories, the repository manager needs to check the remote repository which will incur more overhead than a hosted repository lookup. 希望将hosted repositories【托管资源库】的顺序放在proxy repositories【代理资源库】之前,因为一个group【组资源库】中可以涵括这些托管资源库和代理资源库。而一整个的group是作为一个public,一个接口给别人使用的。所以当查找架包的时候,如果代理资源库在前面,那就是先从远程去查找jar,而不是先从托管资源库(本地仓库)去查找是否有jar。这样访问外网的消耗比起来在本地查找,当然是将托管资源库放在代理资源库之前的优先位置了。4、创建角色创建角色(develop),并分配nx-all权限Security-->Roles-->Create注:创建角色的同时可以为当前创建的角色分配权限。5、创建用户创建用户(developer),并授予develop角色Security-->Users-->Create注:创建用户并为创建的用户挂上相应的角色。(六)maven配置文件Maven下的setting.xml文件和项目中的pom.xml文件的关系是:settting.xml文件是全局设置,而pom.xml文件是局部设置。pom.xml文件对于项目来说,是优先使用的。而pom.xml文件中如果没有配置镜像地址的话,就按照settting.xml中定义的地址去查找。修改本地maven配置文件(conf/setting.xml)servers节点下添加以下内容(username和password为刚刚在nexus3中添加的用户和其密码) <!--nexus服务器,id为组仓库name--> <servers> <server> <id>maven-public</id> <username>developer</username> <password>icoolkj</password> </server> <server> <id>maven-releases</id> <!--对应pom.xml的id=releases的仓库--> <username>developer</username> <password>icoolkj</password> </server> <server> <id>maven-snapshots</id> <!--对应pom.xml中id=snapshots的仓库--> <username>developer</username> <password>icoolkj</password> </server> </servers>mirrors节点下添加以下内容 <!--仓库组的url地址,id和name可以写组仓库name,mirrorOf的值设置为central--> <mirrors> <mirror> <id>maven-public</id> <name>maven-public</name> <!--镜像采用配置好的组的地址--> <url>http://182.92.199.85:8081/repository/maven-public/</url> <mirrorOf>central</mirrorOf> </mirror> </mirrors>(七)项目中发布pom.xml配置实际使用中distributionManagement可以配置在parent项目中,子项目无需重复配置。上述配置全部完成后就可以在项目中使用mven clean deploy将项目的jar包上传到自己的私服上了。 <repositories> <repository> <id>maven-public</id> <name>Nexus Repository</name> <url>http://192.168.1.188:8081/repository/maven-public/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>maven-public</id> <name>Nexus Plugin Repository</name> <url>http://192.168.1.188:8081/repository/maven-public/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </pluginRepository> </pluginRepositories> <!--项目分发信息,在执行mvn deploy后表示要发布的位置。有了这些信息就可以把网站部署到远程服务器或者把构件jar等部署到远程仓库。 --> <distributionManagement> <repository><!--部署项目产生的构件到远程仓库需要的信息 --> <id>maven-releases</id><!-- 此处id和settings.xml的id保持一致 --> <name>Nexus Release Repository</name> <url>http://192.168.1.188:8081/repository/maven-releases/</url> </repository> <snapshotRepository><!--构件的快照部署到哪里?如果没有配置该元素,默认部署到repository元素配置的仓库,参见distributionManagement/repository元素 --> <id>maven-snapshots</id><!-- 此处id和settings.xml的id保持一致 --> <name>Nexus Snapshot Repository</name> <url>http://192.168.1.188:8081/repository/maven-snapshots/</url> </snapshotRepository> </distributionManagement>至此,nexus搭建完毕,支持本地部署依赖jar包。
2022年12月13日
176 阅读
0 评论
0 点赞
2022-12-12
java实现WebSocket服务端
WebSocket服务端配置类import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Service; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * websocket配置类 * @author daiyg * @date 2021/8/24 17:19 */ @Service @Configuration @EnableWebSocket public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); } }服务端代码import com.caso.common.core.utils.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; /** * @author daiyg * @date 2021/8/24 18:15 */ @Component @ServerEndpoint(value = "/websocket/{id}/{key}", subprotocols = {"protocol"}) public class WebSocketServer { private static int onlineCount = 0; private static ConcurrentHashMap<String, WebSocketServer> webSocketSet = new ConcurrentHashMap<>(); private static ConcurrentHashMap<String, List<String>> map = new ConcurrentHashMap<>(); //与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; private static Logger log = LogManager.getLogger(WebSocketServer.class); private String id = ""; private String key= ""; /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(@PathParam(value = "id") String id,@PathParam("key") String key, Session session) { this.session = session; this.id = id;//接收到发送消息的人员编号 this.key = key; if ("1".equals(id)) { log.info("非门岗用户连接!"); } else { List<String> ids = new ArrayList<>(); ids = map.get(id); if(StringUtils.isEmpty(ids)){ ids = new ArrayList<>(); } ids.add(key); map.put(id,ids); webSocketSet.put(key, this); //加入set中 } addOnlineCount(); //在线数加1 log.info("用户" + id + "连接成功!当前在线人数为" + getOnlineCount()); try { sendMessage("连接成功"); } catch (IOException e) { log.error("websocket IO异常"); } } /** * 连接关闭调用的方法 */ @OnClose public void onClose() { //删除map中值 List<String> keys = map.get(this.id); for (int i = 0; i < keys.size(); i++) { if(this.key.equals(keys.get(i))){ keys.remove(i); i--; } } webSocketSet.remove(this.key); //从set中删除 subOnlineCount(); //在线数减1 log.info("有一连接关闭!当前在线人数为" + getOnlineCount()); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 */ @OnMessage public void onMessage(String message) { log.info("来自客户端的消息:" + message); } /** * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error("发生错误"); try { session.close(); } catch (IOException e) { e.printStackTrace(); } error.printStackTrace(); } /** * 服务端主动推送消息 * * @param message * @throws IOException */ public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } /** * 发送信息给指定ID用户,如果用户不在线则返回不在线信息给自己 * * @param message * @param personKeys * @throws IOException */ public static void sendtoUser(String message, String[] personKeys) throws IOException { //获取人员集合 for (String personKey : personKeys) { //获取map中对应的链接 if (map.get(personKey) != null && StringUtils.isNotEmpty(map.get(personKey))) { //遍历给连接人发送信息 for(String s : map.get(personKey)){ webSocketSet.get(s).sendMessage(message); } } else { //如果用户不在线则返回不在线信息给自己 log.error("当前用户不在线:" + personKey); } } } /** * 发送信息给所有人 * * @param * @throws IOException */ public void sendtoAll(String message) throws IOException { for (String key : webSocketSet.keySet()) { try { webSocketSet.get(key).sendMessage(message); } catch (IOException e) { e.printStackTrace(); } } } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketServer.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketServer.onlineCount--; } }
2022年12月12日
83 阅读
0 评论
0 点赞
2022-12-12
Java实现WebSocket服务
一、使用Tomcat提供的WebSocket库 Java可以使用Tomcat提供的WebSocket库接口实现WebSocket服务,代码编写也非常的简单。现在的H5联网游戏基本上都是使用WebSocket协议,基于长连接,服务器可以主动推送消息,而不是传统的网页采用客户端轮询的方式获取服务器的消息。下面给出简单使用Tomcat的WebSocket服务的基本代码结构。@ServerEndpoint("/webSocket") public class WebSocket { @OnOpen public void onOpen(Session session) throws IOException{ logger.debug("新连接"); } @OnClose public void onClose(){ logger.debug("连接关闭"); } @OnMessage public void onMessage(String message, Session session) throws IOException { logger.debug("收到消息"); } @OnError public void onError(Session session, Throwable error){ error.printStackTrace(); } }二、WebSocket协议的整个流程1. 基于TCP协议WebSocket本质是基于TCP协议的,采用Java编写WebSocket服务时可以使用NIO或者AIO实现高并发的服务。2. 握手过程客户端采用TCP协议连接服务器指定端口后,首先需要发送一条HTTP的握手协议GET /web HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: 127.0.0.1:8001 Origin: http://127.0.0.1:8001 Sec-WebSocket-Key: hj0eNqbhE/A0GkBXDRrYYw== Sec-WebSocket-Version: 13请求的头里面必须包含以下内容:Connection 其值为Upgrade,表示升级协议Upgrade 其值为websocket,表示升级为WebSocket协议Sec-WebSocket-Key 客户端发送给服务器的密钥,用于标识每个客户端,其值是16位的随机base64编码。Sec-WebSocket-Version WebSocket的协议版本服务器收到这条协议验证成功后进行协议升级,并且不会关闭Socket连接,并发送给客户端响应升级握手成功的HTTP协议包。HTTP/1.1 101 Switching Protocols Content-Length: 0 Upgrade: websocket Sec-Websocket-Accept: ZEs+c+VBk8Aj01+wJGN7Y15796g= Connection: Upgrade Date: Wed, 21 Jun 2017 03:29:14 GMT 响应的协议包里面,首先是101的状态码,更换协议;其中最重要的就是Sec-WebSocket-Accept字段。其值是通过客户端的Key加上固定的"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"密钥,通过采用16位的base64编码后发送给客户端验证,如果客户端也验证成功就表示握手完成。String acc = secKey + WEBSOCK_MAGIC_TAG; MessageDigest sh1 = MessageDigest.getInstance("SHA1"); String key = Base64.getEncoder().encodeToString(sh1.digest(acc.getBytes()));3. 数据的读写握手成功后就可以进行数据发送和读取,WebSocket的数据可以是二进制或者纯文本。每次读取和发送数据需要打包成帧数据,需要按照其标准的格式进行发送或读取才能够正常的进行数据通信。上图就是帧数据的结构图,解析帧数据的代码如下,由于是摘录的部分代码,所以只能作为理解和参考,不可直接使用。protected WebSocketFrameData ParseFrame(NetPacketBuffer bytes){ bytes.mark(); WebSocketFrameData frame = new WebSocketFrameData(); int opData = bytes.readByte(); frame.UnPackOpCodeHeader(opData); // 第一步 int length = frame.UnPackMaskHeader(bytes.readByte()); // 第二步 // 读取长度 if (length == 126) { length = bytes.readShort(); } else if (length == 127){ length = (int) bytes.readInt64(); } // 数据不足,进来的是半包 if(length + 4 > bytes.remaining()){ bytes.reset(); // return null; } // 读取mask if frame.mMasked byte[] masks = new byte[4]; // 第三步 for (int i = 0; i < 4; i++) { masks[i] = (byte) bytes.readByte(); } frame.mLength = length; frame.mData = bytes.readMulitBytes(length); frame.MaskData(masks); // 第四步 return frame; }上面代码中第一步是解析出当前帧是否是最后帧mFin标记、操作码mOpCode,采用位处理,具体的实现如下。public void UnPackOpCodeHeader(int opData){ mRsv1 = (opData & 64) == 64; mRsv2 = (opData & 32) == 32; mRsv3 = (opData & 16) == 16; mFin = (opData & 128) == 128; mOpCode = (opData & 15); }第二步在读取长度前,先解析当前帧是否有采用Mask掩码加密处理,并且里面有可能包含整个帧的长度信息,具体看上面的判断代码。public int UnPackMaskHeader(int mkData){ mMasked = (mkData & 128) == 128; return (mkData & 127); // 这里返回的是长度信息 }接下来就是读取Mask内容,注意只有客户端发送给服务端时需要采用Mask对数据做处理,服务端发送给客户端时不需要做处理。最后通过Mask掩码解析出真实数据。public void MaskData(byte[] masks){ if (!mMasked or masks.length == 0) return ; for (int i = 0; i < mLength; i++) { mData[i] = (byte) (mData[i] ^ masks[i % 4]); } }以上就解析出单帧的数据,帧数据可以分为消息数据(细分为文本数据和二进制数据)、PING包、PONG包、CLOSE包、CONTINUATION包(数据未发送完成包)。而且帧数据又有mFin标记数据是否完整,否则需要将多个帧数据合成一个完整的消息数据。// 读取帧数据,可能存在多帧数据,因此需要手动拆分 WebSocketFrameData frame = ParseFrame(mCachePacket); if(frame == null){ break; // 说明数据不完整,暂不处理。 } // 不完整的帧的时候,只有第一帧会标记帧的类型 opCode = opCode == -1? frame.mOpCode: opCode; mCacheFrame.append(frame.mData, 0, frame.mLength); if(!frame.mFin) // 非完整的数据不处理。 { continue; } // 处理完整的数据 switch(opCode) { case WebSocketFrameData.OP_TEXT: case WebSocketFrameData.OP_BINARY: mCacheFrame.flip(); this.OnMessage(mCacheFrame, opCode); break; case WebSocketFrameData.OP_PING: this.OnPing(mCacheFrame); break; case WebSocketFrameData.OP_PONG: this.OnPong(mCacheFrame); break; case WebSocketFrameData.OP_CLOSE: this.OnClosed(); break; case WebSocketFrameData.OP_CONTINUATION: this.Close(); break; } opCode = -1; mCacheFrame.clear();读取整个客户端的协议数据流程就已经完成了,服务端发送回去的数据就只需要注意两点:大的数据包需要分帧数据发送。不需要采用Mask掩码加密,因此Mask位置设置为0,并且不写入掩码数据。原文摘自
2022年12月12日
119 阅读
0 评论
0 点赞
2022-12-12
play() failed because the user didn't interact with the document first
问题:在浏览器加载完毕后,自动播放视频:出现错误 play() failed because the user didn't interact with the document first.解决方法:给video标签加入 静音即可。Chrome 66为了避免标签产生随机噪音。声音无法自动播放这个在IOS/Android上面一直是个惯例,桌面版的Safari在2017年的11版本也宣布禁掉带有声音的多媒体自动播放功能,紧接着在2018年4月份发布的Chrome 66也正式关掉了声音自动播放,也就是说 在桌面版浏览器也将失效。最开始移动端浏览器是完全禁止音视频自动播放的,考虑到了手机的带宽以及对电池的消耗。但是后来又改了,因为浏览器厂商发现网页开发人员可能会使用GIF动态图代替视频实现自动播放,正如IOS文档所说,使用GIF的带宽流量是Video(h264)格式的12倍,而播放性能消耗是2倍,所以这样对用户反而是不利的。又或者是使用Canvas进行hack,如Android Chrome文档提到。因此浏览器厂商放开了对多媒体自动播放的限制,只要具备以下条件就能自动播放:(1)没音频轨道,或者设置了muted属性(2)在视图里面是可见的,要插入到DOM里面并且不是display: none或者visibility: hidden的,没有滑出可视区域。换句话说,只要你不开声音扰民,且对用户可见,就让你自动播放,不需要你去使用GIF的方法进行hack.桌面版的浏览器在近期也使用了这个策略,如升级后的Safari 11的说明:这个策略无疑对视频网站的冲击最大,如在Safari打开tudou的提示:添加了一个设置向导。Chrome的禁止更加人性化,它有一个MEI的策略,这个策略大概是说只要用户在当前网页主动播放过超过7s的音视频(视频窗口不能小于200 x 140),就允许自动播放。对于网页开发人员来说,应当如何有效地规避这个风险呢?Chrome的文档给了一个最佳实践:先把音视频加一个muted的属性就可以自动播放,然后再显示一个声音被关掉的按钮,提示用户点一下打开声音。对于视频来说,确实可以这样处理,而对于音频来说,很多人是监听页面点击事件,只要点一次了就开始播放声音,一般就是播放个背景音乐。但是如果对于有多个声音资源的页面来说如何自动播放多个声音呢?首先,如果用户还没进行交互就调用播放声音的API,Chrome会这么提示: DOMException: play() failed because the user didn't interact with the document first. Safari会这么提示: NotAllowedError: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission. Chrome报错提示最为友善,意思是说,用户还没有交互,不能调play。用户的交互包括哪些呢?包括用户触发的touchend, click, doubleclick或者是 keydown事件,在这些事件里面就能调play 所以上面提到很多人是监听整个页面的点击事件进行播放,不管点的哪里,只要点了就行,包括触摸下滑。这种方法只适用于一个声音资源,不适用多个声音,多个声音应该怎么破呢?这里并不是说要和浏览器对着干,“逆天而行”,我们的目的还是为了提升用户体验,因为有些场景如果能自动播放确实比较好,如一些答题的场景,需要听声音进行答题,如果用户在答题的过程中能依次自动播放相应题目的声音,确实比较方便。同时也是讨论声音播放的技术实现。 原生播放视频应该就只能使用video标签,而原生播放音频除了使用audio标签之外,还有另外一个API叫AudioContext,它是能够用来控制声音播放并带了很多丰富的操控接口。调audio.play必须在点击事件里面响应,而使用AudioContext的区别在于只要用户点过页面任何一个地方之后就都能播放了。所以可以用AudioContext取代audio标签播放声音。 我们先用audio.play检测页面是否支持自动播放,以便决定我们播放的时机。1.页面自动播放检测方法很简单,就是创建一个audio元素,给它赋一个src,append到dom里面,然后调用它的play,看是否会抛异常,如果捕获到异常则说明不支持,如下代码所示:function testAutoPlay () { // 返回一个promise以告诉调用者检测结果 return new Promise(resolve => { let audio = document.createElement('audio'); // require一个本地文件,会变成base64格式 audio.src = require('@/assets/empty-audio.mp3'); document.body.appendChild(audio); let autoplay = true; // play返回的是一个promise audio.play().then(() => { // 支持自动播放 autoplay = true; }).catch(err => { // 不支持自动播放 autoplay = false; }).finally(() => { audio.remove(); // 告诉调用者结果 resolve(autoplay); }); }); }这里使用一个空的音频文件,它是一个时间长度为0s的mp3文件,大小只有4kb,并且通过webpack打包成本地的base64格式,所以不用在canplay事件之后才调用play,直接写成同步代码,如果src是一个远程的url,那么就得监听canplay事件,然后在里面play.在告诉调用者结果时,使用Promise resolve的方式,因为play的结果是异步的,并且库函数不推荐使用await.2. 监听页面交互点击如果当前页面能够自动播放,那么可以毫无顾忌地让声音自动播放了,否则就得等到用户开始和这个页面交互了即有点击操作了之后才能自动播放,如下代码所示:let audioInfo = { autoplay: false, testAutoPlay () { // 代码同,略... }, // 监听页面的点击事件,一旦点过了就能autoplay了 setAutoPlayWhenClick () { function setAutoPlay () { // 设置自动播放为true audioInfo.autoplay = true; document.removeEventListener('click', setAutoPlay); document.removeEventListener('touchend', setAutoPlay); } document.addEventListener('click', setCallback); document.addEventListener('touchend', setCallback); }, init () { // 检测是否能自动播放 audioInfo.testAutoPlay().then(autoplay => { if (!audioInfo.autoplay) { audioInfo.autoplay = autoplay; } }); // 用户点击交互之后,设置成能自动播放 audioInfo.setAutoPlayWhenClick(); } }; audioInfo.init(); export default audioInfo; 上面代码主要监听document的click事件,在click事件里面把autoplay值置为true。换句话说,只要用户点过了,我们就能随时调AudioContext的播放API了,即使不是在点击事件响应函数里面,虽然无法在异步回调里面调用audio.play,但是AudioContext可以做到。 代码最后通过调用audioInfo.init,把能够自动播放的信息存储在了audioInfo.autoplay这个变量里面。当需要播放声音的时候,例如切到了下一题,需要自动播放当前题的几个音频资源,就取这个变量判断是否能自动播放,如果能就播,不能就等用户点声音图标自己去播,并且如果他点过了一次之后就都能自动播放了。 那么怎么用AudioContext播放声音呢?3. AudioContext播放声音先请求音频文件,放到ArrayBuffer里面,然后用AudioContext的API进行decode解码,解码完了再让它去play,就行了。我们先写一个请求音频文件的ajax:function request (url) { return new Promise (resolve => { let xhr = new XMLHttpRequest(); xhr.open('GET', url); // 这里需要设置xhr response的格式为arraybuffer // 否则默认是二进制的文本格式 xhr.responseType = 'arraybuffer'; xhr.onreadystatechange = function () { // 请求完成,并且成功 if (xhr.readyState === 4 && xhr.status === 200) { resolve(xhr.response); } }; xhr.send(); }); }这里需要注意的是要把xhr响应类型改成arraybuffer,因为decode需要使用这种存储格式,这样设置之后,xhr.response就是一个ArrayBuffer格式了。接着实例化一个AudioContext,让它去解码然后play,如下代码所示:// Safari是使用webkit前缀 let context = new (window.AudioContext || window.webkitAudioContext)(); // 请求音频数据 let audioMedia = await request(url); // 进行decode和play context.decodeAudioData(audioMedia, decode => play(context, decode));play的函数实现如下:function play (context, decodeBuffer) { let source = context.createBufferSource(); source.buffer = decodeBuffer; source.connect(context.destination); // 从0s开始播放 source.start(0); }这样就实现了AudioContext播放音频的基本功能。如果当前页面是不能autoplay,那么在 new AudioContext的时候,Chrome控制台会报一个警告:这个的意思是说,用户还没有和页面交互你就初始化了一个AudioContext,我是不会让你play的,你需要在用户点击了之后resume恢复这个context才能够进行play.假设我们不管这个警告,直接调用play没有报错,但是没有声音。所以这个时候就要用到上一步audioInfo.autoplay的信息,如果这个为true,那么可以play,否则不能play,需要让用户自己点声音图标进行播放。所以,把代码重新组织一下:function play (context, decodeBuffer) { // 调用resume恢复播放 context.resume(); let source = context.createBufferSource(); source.buffer = decodeBuffer; source.connect(context.destination); source.start(0); } function playAudio (context, url) { let audioMedia = await request(url); context.decodeAudioData(audioMedia, decode => play(context, decode)); } let context = new (window.AudioContext || window.webkitAudioContext)(); // 如果能够自动播放 if (audioInfo.autoplay) { playAudio(url); } // 支持用户点击声音图标自行播放 $('.audio-icon').on('click', function () { playAudio($(this).data('url')); });调了resume之后,如果之前有被禁止播放的音频就会开始播放,如果没有则直接恢复context的自动播放功能。这样就达到基本目的,如果支持自动播放就在代码里面直接play,不支持就等点击。只要点了一次,不管点的哪里接下来的都能够自动播放了。就能实现类似于每隔3s自动播下一题的音频的目的:// 每隔3秒自动播放一个声音 playAudio('question-1.mp3'); setTimeout(() => playAudio(context, 'question-2.mp3'), 3000); setTimeout(() => playAudio(context, 'question-3.mp3'), 3000);这里还有一个问题,怎么知道每个声音播完了,然后再隔个3s播放下一个声音呢?可以通过两个参数,一个是解码后的decodeBuffer有当前音频的时长duration属性,而通过context.currentTime可以知道当前播放时间精度,然后就可以弄一个计时器,每隔100ms比较一下context.currentTime是否大于docode.duration,如果是的话说明播完了。soundjs这个库就是这么实现的,我们可以利用这个库以方便对声音的操作。这样就实现了利用AudioContext自动播放多个音频的目的,限制是用户首次打开页面是不能自动播放的,但是一旦用户点过页面的任何一个地方就可以了。AudioContext还有其它的一些操作。4. AudioContext控制声音属性例如这个CSS Tricks列了几个例子,其中一个是利用AudioContext的振荡器oscillator写了一个电子木琴:这个例子没有用到任何一个音频资源,都是直接合成的,感受如这个Demo:Play the Xylophone (Web Audio API).还有这种混响均衡器的例子:见这个codepen:Web Audio API: parametric equalizer.最后,一直以来都是只有移动端的浏览器禁掉了音视频的自动播放,现在桌面版的浏览器也开始下手了。浏览器这样做的目的在于,不想让用户打开一个页面就各种广告或者其它乱七八糟的声音在播,营造一个纯静的环境。但是浏览器也不是一刀切,至少允许音视频静音的播放。所以对于视频来说,可以静音自动播放,然后加个声音被关掉的图标让用户点击打开,再加添加设置向导之类的方法引导用户设置允许当前网站自动播放。而对于声音可以用AudioContext的API,只要页面被点过一次AudioContext就被激活了,就能直接在代码里面控制播放了。以上可作为当前网页多媒体播放的最佳实践参考。参考链接:https://juejin.im/post/5af7129bf265da0b8262df4c转载于:https://www.cnblogs.com/Neilisme/p/9412315.html
2022年12月12日
105 阅读
0 评论
0 点赞
2022-12-12
uniapp获取图片base64
1.从相册中获取图片uni.chooseImage({ count: 1, // 默认9 sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有 sourceType: ['album'], // 从相册选择 success: (res) => { this.img = res.tempFilePaths } })2.图片转成base64uni.getFileSystemManager().readFile({ filePath: this.img[0], encoding: 'base64', success: r => { console.log("base64===="+r.data) }, fail: (errr) => { uni.hideLoading() } })
2022年12月12日
256 阅读
0 评论
0 点赞
1
...
4
5
6
...
14
您的IP: