Android 使用OpenGLES + MediaPlayer 获取视频截图

在这里插入图片描述

概述

    Android 获取视频缩略图的方法通常有:

  1. ContentResolver: 使用系统数据库
  2. MediaMetadataRetriever: 这个是android提供的类,用来获取本地和网络media相关文件的信息
  3. ThumbnailUtils: 是在android2.2(api8)之后新增的一个,该类为我们提供了三个静态方法供我们使用。
  4. MediaExtractor 与MediaMetadataRetriever类似
    然而, 这几种方法存在一定的局限性, 比如, ContentResolver需要视频文件已经通过mediascanner 添加到系统数据库中, 使用MediaMetadataRetriever不支持某些格式等等. 常规的格式比如MP4, MKV, 这些接口还是很实用的.

    对于系统不支持的播放的格式比如AVI等, 需要一个更丰富的接口或方法来获取视频的缩略图. 于是尝试使用OpenGLES 离屏渲染 + MediaPlayer来提取视频画面作为缩略图.

参考代码

参考:ExtractMpegFramesTest.java 改动, 使用MediaPlayer, 理论上, 只要使用MediaPlayer可以播放的视频, 都可以提取出视频画面.


import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.SurfaceTexture;
import android.media.MediaPlayer;
import android.opengl.EGL14;
import android.opengl.EGLConfig;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLSurface;
import android.opengl.GLES10;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.os.Build;
import android.util.Log;
import android.view.Surface;import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;import javax.microedition.khronos.opengles.GL10;public class VideoFrameExtractorGL extends Thread implements MediaPlayer.OnSeekCompleteListener {final String TAG = "VFEGL";private MediaPlayer mediaPlayer;private Surface surface;private int bitmapWidth, bitmapHeight;private int textureId;private SurfaceTexture surfaceTexture;final Object drawLock = new Object();// Shader代码private String vertexShaderCode ="#version 300 es\n" +"in vec4 position;\n" +"in vec2 texCoord;\n" +"uniform mat4 uSTMatrix;\n" +"out vec2 vTexCoord;\n" +"\n" +"void main() {\n" +"    // \n" +"    float curvature = -0.5; // 曲率值,负值表示凹面\n" +"    vec4 pos = position;\n" +"    //pos.z = curvature * (pos.x * pos.x + pos.y * pos.y);\n" +"\n" +"    //if(pos.x > 0.0001) pos.y += 0.2;\n" +"\n" +"    gl_Position = pos;\n" +"    vTexCoord = (uSTMatrix * vec4(texCoord, 0.0, 1.0)).xy;\n" +"}";private String fragmentShaderCode ="#version 300 es\n" +"#extension GL_OES_EGL_image_external : require\n" +"precision mediump float;\n" +"\n" +"in vec2 vTexCoord;\n" +"uniform samplerExternalOES sTexture;\n" +"out vec4 fragColor;\n" +"\n" +"void main() {\n" +"    fragColor = texture(sTexture, vTexCoord);\n" +"}\n";protected float[] mSTMatrix = new float[16];protected int mProgram;private int mPositionHandle;private int mTexCoordHandle;private int mSTMatrixHandle;// 顶点坐标和纹理坐标private final float[] squareCoords = {-1.0f,  1.0f,   // top left1.0f,  1.0f,   // top right-1.0f, -1.0f,   // bottom left1.0f, -1.0f    // bottom right};private final float[] textureCoords = {0.0f, 0.0f,   // top left1.0f, 0.0f,   // top right0.0f, 1.0f,   // bottom left1.0f, 1.0f    // bottom right};private FloatBuffer vertexBuffer;private FloatBuffer textureBuffer;// EGL相关变量private EGLDisplay eglDisplay;private EGLContext eglContext;private EGLSurface eglSurface;boolean isPrepared = false;long videoDuration = 0;int videoWidth, videoHeight;String mPath;// 构造函数,初始化MediaPlayer并设置渲染大小public VideoFrameExtractorGL(String videoFile, int bitmapWidth, int bitmapHeight, OnFrameExtractListener l) {this.bitmapWidth = bitmapWidth;this.bitmapHeight = bitmapHeight;mPath = videoFile;setOnFrameExtractListener(l);}@Overridepublic void run() {mediaPlayer = new MediaPlayer();mediaPlayer.setVolume(0, 0);mediaPlayer.setOnSeekCompleteListener(this);initializeEGL();   // 初始化EGL环境initializeOpenGL(); // 初始化OpenGL离屏渲染环境try {mediaPlayer.setDataSource(mPath);mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {@Overridepublic void onPrepared(MediaPlayer mediaPlayer) {Log.d(TAG, "onPrepared start playing");isPrepared = true;videoDuration = mediaPlayer.getDuration();videoWidth = mediaPlayer.getVideoWidth();videoHeight = mediaPlayer.getVideoHeight();mediaPlayer.start();}});mediaPlayer.prepareAsync();} catch (Exception e) {e.printStackTrace();}int timeout = 20;while(!isPrepared){try {sleep(30);timeout--;if(timeout < 0){break;}} catch (InterruptedException ignore) {}}while(mediaPlayer != null) {drawFrameLoop();}}// 初始化EGL环境@SuppressLint("NewApi")private void initializeEGL() {Log.d(TAG, "initializeEGL");// 1. 获取默认的EGL显示设备eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);if (eglDisplay == EGL14.EGL_NO_DISPLAY) {throw new RuntimeException("Unable to get EGL14 display");}// 2. 初始化EGLint[] version = new int[2];if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) {throw new RuntimeException("Unable to initialize EGL14");}// 3. 配置EGLint[] configAttributes = {EGL14.EGL_RED_SIZE, 8,EGL14.EGL_GREEN_SIZE, 8,EGL14.EGL_BLUE_SIZE, 8,EGL14.EGL_ALPHA_SIZE, 8,EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,EGL14.EGL_NONE};EGLConfig[] eglConfigs = new EGLConfig[1];int[] numConfigs = new int[1];EGL14.eglChooseConfig(eglDisplay, configAttributes, 0, eglConfigs, 0, 1, numConfigs, 0);if (numConfigs[0] == 0) {throw new IllegalArgumentException("No matching EGL configs");}EGLConfig eglConfig = eglConfigs[0];// 4. 创建EGL上下文int[] contextAttributes = {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,EGL14.EGL_NONE};eglContext = EGL14.eglCreateContext(eglDisplay, eglConfig, EGL14.EGL_NO_CONTEXT, contextAttributes, 0);if (eglContext == null) {throw new RuntimeException("Failed to create EGL context");}// 5. 创建离屏渲染的EGL Surfaceint[] surfaceAttributes = {EGL14.EGL_WIDTH, bitmapWidth,EGL14.EGL_HEIGHT, bitmapHeight,EGL14.EGL_NONE};eglSurface = EGL14.eglCreatePbufferSurface(eglDisplay, eglConfig, surfaceAttributes, 0);if (eglSurface == null) {throw new RuntimeException("Failed to create EGL Surface");}// 6. 绑定上下文if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {throw new RuntimeException("Failed to bind EGL context");}}// 初始化OpenGL ES的离屏渲染,使用帧缓冲区private void initializeOpenGL() {Log.d(TAG, "initializeOpenGL");// 创建纹理并绑定int[] textureIds = new int[1];GLES20.glGenTextures(1, textureIds, 0);textureId = textureIds[0];// 绑定纹理并绘制//GLES20.glActiveTexture(GLES20.GL_TEXTURE0);GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR);GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE);GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE);surfaceTexture = new SurfaceTexture(textureId);surfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {@Overridepublic void onFrameAvailable(SurfaceTexture surfaceTexture) {Log.d(TAG, "onFrameAvailable");synchronized (drawLock){drawLock.notifyAll();}}});// 创建Surface与MediaPlayer绑定surface = new Surface(surfaceTexture);mediaPlayer.setSurface(surface);// 初始化着色器createProgram(vertexShaderCode, fragmentShaderCode);// 在构造函数中初始化缓冲区vertexBuffer = ByteBuffer.allocateDirect(squareCoords.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().put(squareCoords);vertexBuffer.position(0);textureBuffer = ByteBuffer.allocateDirect(textureCoords.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().put(textureCoords);textureBuffer.position(0);}// 创建着色器程序private void createProgram(String vertexSource, String fragmentSource) {int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);mProgram = GLES20.glCreateProgram();GLES20.glAttachShader(mProgram, vertexShader);GLES20.glAttachShader(mProgram, fragmentShader);GLES20.glLinkProgram(mProgram);GLES20.glUseProgram(mProgram);mPositionHandle = GLES20.glGetAttribLocation(mProgram, "position");mTexCoordHandle = GLES20.glGetAttribLocation(mProgram, "texCoord");mSTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uSTMatrix");}// 加载着色器private int loadShader(int type, String shaderSource) {int shader = GLES20.glCreateShader(type);GLES20.glShaderSource(shader, shaderSource);GLES20.glCompileShader(shader);return shader;}final Object extractLock = new Object();int targetPosOfVideo;boolean seeking = false;//if ignore time check, add extracting to check if notify callback//or else, it will notify after player is started.boolean extracting = false;@SuppressLint("WrongConstant")public void extract(int posOfVideoInMs){synchronized (extractLock) {targetPosOfVideo = posOfVideoInMs;seeking = true;extracting = true;while (!isPrepared) {Log.w(TAG, "extract " + posOfVideoInMs + " ms failed: MediaPlayer is not ready.");try {Thread.sleep(15);} catch (InterruptedException ignore) {}}{if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {mediaPlayer.seekTo(posOfVideoInMs, MediaPlayer.SEEK_NEXT_SYNC);}else{mediaPlayer.seekTo(posOfVideoInMs);}}try {Log.d(TAG, "extract " + posOfVideoInMs + " ms, and start extractLock wait");extractLock.wait();} catch (InterruptedException e) {e.printStackTrace();}}}public void extract(float posRatio){while(!isPrepared){try {sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}}int pos = (int)(posRatio * videoDuration);extract(pos);}// 截取指定时间的图像帧private void drawFrameLoop() {synchronized (drawLock) {long pos = mediaPlayer.getCurrentPosition();Log.d(TAG, "drawFrameLoop at " + pos  + " ms");surfaceTexture.updateTexImage();surfaceTexture.getTransformMatrix(mSTMatrix);GLES20.glViewport(0, 0, bitmapWidth, bitmapHeight);GLES10.glClearColor(color[0], color[1], color[2], 1.0f); // 设置背景颜色为黑色GLES10.glClear(GL10.GL_COLOR_BUFFER_BIT); // 清除颜色缓冲区GLES20.glUseProgram(mProgram);// 传递顶点数据vertexBuffer.position(0);GLES20.glEnableVertexAttribArray(mPositionHandle);GLES20.glVertexAttribPointer(mPositionHandle, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer);// 传递纹理坐标数据textureBuffer.position(0);GLES20.glEnableVertexAttribArray(mTexCoordHandle);GLES20.glVertexAttribPointer(mTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0, textureBuffer);// 传递纹理矩阵GLES20.glUniformMatrix4fv(mSTMatrixHandle, 1, false, mSTMatrix, 0);// 绘制纹理GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, vertexBuffer.limit() / 2);Bitmap bm = toBitmap();if(extracting && !seeking) {boolean notifyCallback = ignoreTimeCheck || (targetPosOfVideo > 0 && pos >= targetPosOfVideo);if(notifyCallback) {targetPosOfVideo = 0;if (oel != null) oel.onFrameExtract(this, bm);synchronized (extractLock) {Log.d(TAG, "drawFrameLoop notify extractLock");extractLock.notify();}}}try{drawLock.wait();}catch(Exception ignore){}}}public boolean isDone(){return mediaPlayer != null && targetPosOfVideo <= mediaPlayer.getCurrentPosition();}public Bitmap getBitmap(){return toBitmap();}IntBuffer pixelBuffer;private Bitmap toBitmap(){// 读取帧缓冲区中的像素if(pixelBuffer == null){pixelBuffer = IntBuffer.allocate(bitmapWidth * bitmapHeight);}else {pixelBuffer.rewind();}GLES20.glReadPixels(0, 0, bitmapWidth, bitmapHeight, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixelBuffer);// 创建Bitmap并将像素数据写入Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);pixelBuffer.position(0);bitmap.copyPixelsFromBuffer(pixelBuffer);return bitmap;}// 释放资源@SuppressLint("NewApi")public void release() {if (mediaPlayer != null) {mediaPlayer.release();}mediaPlayer = null;synchronized (drawLock){try {drawLock.wait(100);} catch (InterruptedException e) {e.printStackTrace();}drawLock.notifyAll();}if (surface != null) {surface.release();}if (eglSurface != null) {EGL14.eglDestroySurface(eglDisplay, eglSurface);}if (eglContext != null) {EGL14.eglDestroyContext(eglDisplay, eglContext);}if (eglDisplay != null) {EGL14.eglTerminate(eglDisplay);}GLES20.glDeleteTextures(1, new int[]{textureId}, 0);}float[] color = {0, 0, 0, 1f};public void setColor(float r, float g, float b){color[0] = r; color[1] = g; color[2] = b;}OnFrameExtractListener oel;public void setOnFrameExtractListener(OnFrameExtractListener l){oel = l;}@Overridepublic void onSeekComplete(MediaPlayer mediaPlayer) {Log.d(TAG, "onSeekComplete pos=" + mediaPlayer.getCurrentPosition());seeking = false;}public interface OnFrameExtractListener{void onFrameExtract(VideoFrameExtractorGL vfe, Bitmap bm);}public static Bitmap[] extractBitmaps(int[] targetMs, String path, int bmw, int bmh){final int len = targetMs.length;final Bitmap[] bms = new Bitmap[len];VideoFrameExtractorGL vfe = new VideoFrameExtractorGL(path, bmw, bmh, new OnFrameExtractListener() {int count = 0;@Overridepublic void onFrameExtract(VideoFrameExtractorGL vfe, Bitmap bm) {Log.d("VFEGL", "extractBitmaps");bms[count] = bm;count ++;if(count >= len){vfe.release();}}});vfe.start();for(int pos : targetMs){vfe.extract(pos);}Log.d("VFEGL", "extractBitmaps done");return bms;}boolean ignoreTimeCheck = false;public static Bitmap[] extractBitmaps(String path, int bmw, int bmh,final float durationRatio, final int bitmapCount){final Bitmap[] bms = new Bitmap[bitmapCount];VideoFrameExtractorGL vfe = new VideoFrameExtractorGL(path, bmw, bmh, new OnFrameExtractListener() {int count = 0;@Overridepublic void onFrameExtract(VideoFrameExtractorGL vfe, Bitmap bm) {Log.d("VFEGL", "extractBitmaps");bms[count] = bm;count ++;if(count >= bitmapCount){vfe.release();}}});vfe.start();vfe.ignoreTimeCheck = true;vfe.extract(durationRatio);Log.d("VFEGL", "extractBitmaps done");return bms;}
}

基本的流程如下:

  1. 初始化MeidaPlayer 用与播放视频
  2. 初始化OpenGL环境, 绑定Texture 和 SurfaceTexture
  3. 使用SurfaceTexutre创建Surface, 并为MediaPlayer 设置Surface, 这样视频就会绘制到Surface上
  4. 通过SurfaceTexture的setOnFrameAvailableListener回调绘制帧数据
  5. 从OpenGL中提取出glReadPixels提取出像素数据, 填充到Bitmap

调用

		Bitmap[] bms = VideoFrameExtractorGL.extractBitmaps(path, 128, 72, 0.5f, bmCount);

需注意:

  1. 注意OpenGLES 的版本, 1.x 不支持离屏渲染, 2.x 需要配合着色器渲染图像
  2. 构建OpenGL的环境和渲染的工作, 要放在同一个线程中.

参考

Kotlin拿Android本地视频缩略图
Android视频图片缩略图的获取
Android 获取视频缩略图(获取视频每帧数据)的优化方案
37.4 OpenGL离屏渲染环境配置
ExtractMpegFramesTest.java

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

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

相关文章

博泽Brose EDI项目案例

Brose 是一家德国的全球性汽车零部件供应商&#xff0c;主要为全球汽车制造商提供机电一体化系统和组件&#xff0c;涵盖车门、座椅调节系统、空调系统以及电动驱动装置等。Brose 以其高质量的创新产品闻名&#xff0c;在全球拥有多个研发和生产基地&#xff0c;是全球第五大家…

python使用python-docx处理word

文章目录 一、python-docx简介二、基本使用1、新建与保存word2、写入Word&#xff08;1&#xff09;打开文档&#xff08;2&#xff09;添加标题&#xff08;3&#xff09;添加段落&#xff08;4&#xff09;添加文字块&#xff08;5&#xff09;添加图片&#xff08;6&#xf…

aws(学习笔记第十五课) 如何从灾难中恢复(recover)

aws(学习笔记第十五课) 如何从灾难中恢复 学习内容&#xff1a; 使用CloudWatch对服务器进行监视与恢复区域(region)&#xff0c;可用区(available zone)和子网(subnet)使用自动扩展(AutoScalingGroup) 1. 使用CloudWatch对服务器进行监视与恢复 整体架构 这里模拟Jenkins Se…

lwip raw、netcoon、socket三种接口编程的区别

目录 一、前言 二、LWIP 简介 三、LWIP RAW 编程 1.概念与原理 2.编程模型与流程 3.示例代码 4.优点与缺点 四、LWIP NETCONN 编程 1.概念与原理 2.编程模型与流程 3.示例代码 4.优点与缺点 五、LWIP SOCKET 编程 1.概念与原理 2.编程模型与流程 3.示例代码 …

【XGlassTerminal.js】快速 构建 炫酷 终端 网页 以及 Linux 模拟器 在线!!

XGlassTerminal.js XGlassTerminal.js 是一个用于构建前端终端样式的 JavaScript 库。它允许开发者轻松地创建一个具有终端风格的用户界面&#xff0c;并对用户输入的命令进行事件处理。 该库提供了丰富的功能&#xff0c;包括文本添加、命令处理、点击事件绑定等。 同时还支…

车辆传动系统的simulink建模与仿真,分析加速和刹车两个工况

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 5.完整工程文件 1.课题概述 车辆传动系统的simulink建模与仿真,分析加速和刹车两个工况。模型包括车辆模块&#xff0c;传动模块&#xff0c;发动机模块&#xff0c;换挡模块&#xff0c;刹车油门输入模块…

宝塔配置定时任务详解

文章目录 宝塔配置定时任务详解一、引言二、配置定时任务1、登录宝塔面板2、添加定时任务2.1、步骤 3、配置任务3.1、设置任务名称和执行周期3.2、设置执行命令 4、保存并测试 三、使用示例1、备份数据库2、清理日志文件 四、总结 宝塔配置定时任务详解 一、引言 在服务器管理…

【C++笔记】map和set的使用

【C笔记】map和set的深度剖析 &#x1f525;个人主页&#xff1a;大白的编程日记 &#x1f525;专栏&#xff1a;C笔记 文章目录 【C笔记】map和set的深度剖析前言一.set1.1 序列式容器和关联式容器1.2 set系列的使用1.3 set类的介绍1.4 set的构造和迭代器1.5 set的增删查1.6…

springboot+mybatis对接使用postgresql中PostGIS地图坐标扩展类型字段

方案一&#xff08;完全集成和自动解析&#xff09;&#xff1a; <dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId></dependency> 使用 org.postgresql.geometric包下的 PGpoint 类来接收数据库中POINT…

《只狼》运行时提示“mfc140u.dll文件缺失”是什么原因?“找不到mfc140u.dll文件”要怎么解决?教你几招轻松搞定

《只狼》运行时提示“mfc140u.dll文件缺失”的科普与解决方案 作为一名软件开发从业者&#xff0c;在游戏开发和维护过程中&#xff0c;我们经常会遇到各种运行时错误和系统报错。今天&#xff0c;我们就来探讨一下《只狼》这款游戏在运行时提示“mfc140u.dll文件缺失”的原因…

华为HarmonyOS 让应用快速拥有账号能力 -- 3 获取用户手机号

场景介绍 当应用对获取的手机号时效性要求不高时&#xff0c;可使用Account Kit提供的手机号授权与快速验证能力&#xff0c;向用户发起手机号授权申请&#xff0c;经用户同意授权后&#xff0c;获取到手机号并为用户提供相应服务。以下只针对Account kit提供的手机号授权与快…

linux环境人大金仓数据库修改密码

1.进入人大金仓安装目录 cd /home/opt/Kingbase/ES/V9/Server/bin2.连接数据库 ./ksql -U system -d mydb -h 127.0.0.1 -p 54321-u 用户名 -d 数据库名 -h ip地址 -p 端口号 3.修改密码 ALTER USER system WITH PASSWORD 密码;

使用R语言进行美国失业率时空分析(包括绘图)

今天写一篇利用R语言&#xff0c;针对面板数据的简单分析与绘图。让我们直接开始把。 一、数据准备 这次的示例数据非常简单&#xff0c;只有一个shp格式的美国区县矢量数据&#xff0c;我们在QGIS中打开数据查看一下它的属性表。事实上我们需要的数据都在属性表的字段中。 二…

单片机几大时钟源

在单片机中&#xff0c;MSI、HSI和HSE通常指的是用于内部晶振配置的不同功能模块&#xff1a; MSI (Master Oscillator System Interface)&#xff1a;这是最低级的一种时钟源管理单元&#xff0c;它控制着最基本的系统时钟&#xff08;SYSCLK&#xff09;&#xff0c;一般由外…

前端开发 之 15个页面加载特效上【附完整源码】

文章目录 一&#xff1a;彩球环绕加载特效1.效果展示2.HTML完整代码 二&#xff1a;跷跷板加载特效1.效果展示2.HTML完整代码 三&#xff1a;两个圆形加载特效1.效果展示2.HTML完整代码 四&#xff1a;半环加载特效1.效果展示2.HTML完整代码 五&#xff1a;音乐波动加载特效1.效…

Spring入园须知

序 聊 Spring&#xff0c;先从发展历史谈起&#xff0c;对整个生态有个大致认识&#xff0c;最后再看下 Spring 依赖的基础机制——IoC 和 AOP&#xff0c;就达到入门须知的目的了。毕竟 Spring 太大了&#xff0c;如果把话题扯得太宽泛太细节&#xff0c;很可能会迷失在 Spri…

作品截图_

openstack project create --domain default --description "姓氏" xingopenstack user create --domain default --password-prompt --description "名字" mingziopenstack role create --description "姓名首字母" xmzopenstack role add --pr…

使用API管理Dynadot域名,设置默认域名服务器ip信息

前言 Dynadot是通过ICANN认证的域名注册商&#xff0c;自2002年成立以来&#xff0c;服务于全球108个国家和地区的客户&#xff0c;为数以万计的客户提供简洁&#xff0c;优惠&#xff0c;安全的域名注册以及管理服务。 Dynadot平台操作教程索引&#xff08;包括域名邮箱&…

聚合支付系统官方个人免签系统三方支付系统稳定安全高并发

系统采用fastadmin框架独立全新开发&#xff0c;安全稳定,系统支持代理、商户、码商等业务逻辑。 针对最近一-些JD&#xff0c;TB等业务定制&#xff0c;子账号业务逻辑API 非常详细&#xff0c;方便内置对接! 注意&#xff1a;系统没有配置文档很使用教程&#xff0c;不清楚…

vue结合canvas动态生成水印效果

在 Vue 项目中添加水印可以通过以下几种方式实现&#xff1a; 方法一&#xff1a;使用 CSS 直接通过 CSS 的 background 属性实现水印&#xff1a; 实现步骤 在需要添加水印的容器中设置背景。使用 rgba 设置透明度&#xff0c;并通过 background-repeat 和 background-size…