FFmpeg进阶: 截取视频生成gif动图

news2025/1/20 7:24:06

文章目录

    • 1.封装视频滤镜
    • 2.截取视频生成gif
    • 3.gif优化
    • 4.示例效果

现在互联网上很多人都通过表情包来表达自己的情绪,常用的表情包很多都是视频文件的一部分。这里就介绍一下如何通过ffmpeg截取视频生成gif动图。其实原理很简单,首先我们seek到视频对应的位置,然后读取数据帧修改帧的数据格式并输出到gif文件当中,读取完毕之后我们就得到了一个视频动图。具体的操作步骤如下:

1.封装视频滤镜

首先封装一下视频滤镜,方便对数据帧进行变换处理

//video_filter.h
#ifndef VIDEOBOX_VIDEO_FILTER_H
#define VIDEOBOX_VIDEO_FILTER_H

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavfilter/avfilter.h>
#include <libavfilter/buffersrc.h>
#include <libavfilter/buffersink.h>
#include <libavutil/channel_layout.h>
#include <libavutil/opt.h>
}

//滤镜输入和输出对应的配置
struct VideoConfig {
    AVPixelFormat format;
    int width;
    int height;
    AVRational timebase{1, 30};
    AVRational pixel_aspect{1, 1};

    VideoConfig(AVPixelFormat format, int width, int height, AVRational timebase = {1, 30},
                AVRational pixel_aspect = {1, 1}) {
        this->format = format; //像素格式
        this->width = width;   //视频宽
        this->height = height; //视频高
    }
};

class VideoFilter {
protected:
    AVFilterContext *buffersink_ctx;
    AVFilterContext *buffersrc_ctx;
	AVFilterContext *buffersrc_ctx1;
    AVFilterGraph *filter_graph;
    const char *description = nullptr;
public:

    VideoFilter() = default;

	//构建对应的滤镜
    int create(const char *filter_descr, VideoConfig *inConfig, VideoConfig *outConfig);
	int create(const char *filter_descr, VideoConfig *inConfig1, VideoConfig *inConfig2, VideoConfig *outConfig);

	//获取滤镜的输入和输出
    int filter(AVFrame *source, AVFrame *dest);
	int filter(AVFrame* source1, AVFrame* source2, AVFrame*dest);
	

	//添加输入1
	int addInput1(AVFrame * input);

	//添加输入2
	int addInput2(AVFrame* input);

	//获取处理之后的结果
	int getFrame(AVFrame* result);

    void dumpGraph();

    void destroy();
};

#endif 

//video_filter.cpp
#include "video_filter.h"

int VideoFilter::create(const char *filter_descr, VideoConfig *inConfig, VideoConfig *outConfig) {
    this->description = filter_descr;
    char args[512];
    int ret = 0;
    const AVFilter *buffersrc = avfilter_get_by_name("buffer");
    const AVFilter *buffersink = avfilter_get_by_name("buffersink");
    AVFilterInOut *outputs = avfilter_inout_alloc();
    AVFilterInOut *inputs = avfilter_inout_alloc();
    enum AVPixelFormat pix_fmts[] = {outConfig->format, AV_PIX_FMT_NONE};

    filter_graph = avfilter_graph_alloc();
    if (!outputs || !inputs || !filter_graph) {
        ret = AVERROR(ENOMEM);
        goto end;
    }

    snprintf(args, sizeof(args),
             "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
             inConfig->width, inConfig->height, inConfig->format,
             inConfig->timebase.num, inConfig->timebase.den,
             inConfig->pixel_aspect.num, inConfig->pixel_aspect.num);
    ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
                                       args, nullptr, filter_graph);
    if (ret < 0) {
        goto end;
    }

    ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",
                                       nullptr, nullptr, filter_graph);
    if (ret < 0) {
        goto end;
    }

    ret = av_opt_set_int_list(buffersink_ctx, "pix_fmts", pix_fmts,
                              AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
    if (ret < 0) {
        goto end;
    }

    outputs->name = av_strdup("in");
    outputs->filter_ctx = buffersrc_ctx;
    outputs->pad_idx = 0;
    outputs->next = nullptr;

    inputs->name = av_strdup("out");
    inputs->filter_ctx = buffersink_ctx;
    inputs->pad_idx = 0;
    inputs->next = nullptr;

    if ((ret = avfilter_graph_parse_ptr(filter_graph, filter_descr,
                                        &inputs, &outputs, nullptr)) < 0) {
        goto end;
    }

    if ((ret = avfilter_graph_config(filter_graph, nullptr)) < 0) {
        goto end;
    }

    end:
    avfilter_inout_free(&inputs);
    avfilter_inout_free(&outputs);

    return ret;
}


int VideoFilter::create(const char *filter_descr, VideoConfig *inConfig1, VideoConfig *inConfig2, VideoConfig *outConfig) {
	this->description = filter_descr;
	char args1[512];
	char args2[512];
	int ret = 0;
	const AVFilter *buffersrc1 = avfilter_get_by_name("buffer");
	const AVFilter *buffersrc2 = avfilter_get_by_name("buffer");
	const AVFilter *buffersink = avfilter_get_by_name("buffersink");
	AVFilterInOut *outputs = avfilter_inout_alloc();
	AVFilterInOut *inputs = avfilter_inout_alloc();
	AVFilterInOut *full_output = avfilter_inout_alloc();
	enum AVPixelFormat pix_fmts[] = { outConfig->format, AV_PIX_FMT_NONE };

	filter_graph = avfilter_graph_alloc();
	if (!outputs || !inputs || !filter_graph || !full_output) {
		ret = AVERROR(ENOMEM);
		goto end;
	}

	snprintf(args1, sizeof(args1),
		"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
		inConfig1->width, inConfig1->height, inConfig1->format,
		inConfig1->timebase.num, inConfig1->timebase.den,
		inConfig1->pixel_aspect.num, inConfig1->pixel_aspect.num);


	snprintf(args2, sizeof(args2),
		"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
		inConfig2->width, inConfig2->height, inConfig2->format,
		inConfig2->timebase.num, inConfig2->timebase.den,
		inConfig2->pixel_aspect.num, inConfig2->pixel_aspect.num);

	ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc1, "in",
		args1, nullptr, filter_graph);
	if (ret < 0) {
		goto end;
	}
	ret = avfilter_graph_create_filter(&buffersrc_ctx1, buffersrc2, "in1",
		args2, nullptr, filter_graph);
	if (ret < 0) {
		goto end;
	}


	ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",
		nullptr, nullptr, filter_graph);
	if (ret < 0) {
		goto end;
	}

	ret = av_opt_set_int_list(buffersink_ctx, "pix_fmts", pix_fmts,
		AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
	if (ret < 0) {
		goto end;
	}

	outputs->name = av_strdup("in");
	outputs->filter_ctx = buffersrc_ctx;
	outputs->pad_idx = 0;
	outputs->next = full_output;

	full_output->name = av_strdup("in1");
	full_output->pad_idx = 0;
	full_output->filter_ctx = buffersrc_ctx1;
	full_output->next = NULL;

	inputs->name = av_strdup("out");
	inputs->filter_ctx = buffersink_ctx;
	inputs->pad_idx = 0;
	inputs->next = nullptr;

	if ((ret = avfilter_graph_parse_ptr(filter_graph, filter_descr,
		&inputs, &outputs, nullptr)) < 0) {
		goto end;
	}

	if ((ret = avfilter_graph_config(filter_graph, nullptr)) < 0) {
		goto end;
	}

end:
	avfilter_inout_free(&inputs);
	avfilter_inout_free(&outputs);

	return ret;
}

int VideoFilter::filter(AVFrame *source, AVFrame *dest) {

    int ret = av_buffersrc_add_frame_flags(buffersrc_ctx, source, AV_BUFFERSRC_FLAG_KEEP_REF);
    if (ret < 0) {
        return -1;
    }
    ret = av_buffersink_get_frame(buffersink_ctx, dest);
    if (ret < 0) {
        return -1;
    }
    return 0;
}

int VideoFilter::filter(AVFrame * source1, AVFrame * source2, AVFrame * dest)
{
	int ret = av_buffersrc_add_frame_flags(buffersrc_ctx, source1, AV_BUFFERSRC_FLAG_KEEP_REF);
	if (ret < 0) {
		return -1;
	}

	ret = av_buffersrc_add_frame_flags(buffersrc_ctx1, source2, AV_BUFFERSRC_FLAG_KEEP_REF);
	if (ret < 0) {
		return -1;
	}

	ret = av_buffersink_get_frame(buffersink_ctx, dest);
	if (ret < 0) {
		return -1;
	}
	return 0;
}

int VideoFilter::addInput1(AVFrame * input)
{
	return av_buffersrc_add_frame_flags(buffersrc_ctx, input, AV_BUFFERSRC_FLAG_KEEP_REF);
}

int VideoFilter::addInput2(AVFrame * input)
{
	return av_buffersrc_add_frame_flags(buffersrc_ctx1, input, AV_BUFFERSRC_FLAG_KEEP_REF);
}

int VideoFilter::getFrame(AVFrame * result)
{
	return av_buffersink_get_frame(buffersink_ctx, result);
}

void VideoFilter::dumpGraph() {
   printf("%s:%s", description, avfilter_graph_dump(filter_graph, nullptr));
}

void VideoFilter::destroy() {
    if (filter_graph)
        avfilter_graph_free(&filter_graph);
}

2.截取视频生成gif

封装完毕视频滤镜之后,我们就可以读取视频的数据帧,修改数据结构输出gif动图了,对应的实现如下:


#pragma execution_character_set("utf-8")
#define _CRT_SECURE_NO_WARNINGS

#include <string>
#include <iostream>
#include <thread>
#include <memory>
#include <iostream>
#include <fstream>

extern "C"
{
#include "libavutil/opt.h"
#include <libavcodec/avcodec.h>
#include <libavfilter/avfilter.h>
#include <libavformat/avformat.h>
#include <libavformat/avio.h>
#include <libavutil/avutil.h>
#include <libswresample/swresample.h>
#include <libswscale/swscale.h>
#include <libavutil/frame.h>
#include <libavutil/time.h>
#include <libavfilter/buffersink.h>
#include <libavfilter/buffersrc.h>
#include <libavutil/imgutils.h>
}
#include "video_filter.h"

//@1输入视频文件的地址
//@2输出gif文件的地址
//@3起始时间(s)
//@4结束时间(s)
int convert_video_to_gif(const char *output_filename, const char *input_filename, float from, float to) 
{
	
	if (from < 0 || from >= to) {
		return -1;
	}

	AVFormatContext *inFmtCtx = nullptr;
	AVFormatContext *outFmtCtx = nullptr;
	AVCodecContext *videoCodecCtx = nullptr;
	AVCodecContext *gifCodecCtx = nullptr;

	int ret = 0;

	//打开输入文件
	ret = avformat_open_input(&inFmtCtx, input_filename, nullptr, nullptr);
	ret = avformat_find_stream_info(inFmtCtx, nullptr);
	
	ret = avformat_alloc_output_context2(&outFmtCtx, nullptr, nullptr, output_filename);

	int video_idx = 0;

	for (int i = 0; i < inFmtCtx->nb_streams; ++i) 
	{
		if (inFmtCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) 
		{
			video_idx = i;
			AVStream *inVideoStream = inFmtCtx->streams[i];
			AVStream *outVideoStream = avformat_new_stream(outFmtCtx, nullptr);
	
			//创建输出流和对应的编码器
			av_dict_copy(&outVideoStream->metadata, inVideoStream->metadata, 0);
			const AVCodec *inCodec = avcodec_find_decoder(inVideoStream->codecpar->codec_id);
			videoCodecCtx = avcodec_alloc_context3(inCodec);
			ret = avcodec_parameters_to_context(videoCodecCtx, inVideoStream->codecpar);
			ret = avcodec_open2(videoCodecCtx, inCodec, nullptr);
			
			const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_GIF);
			gifCodecCtx = avcodec_alloc_context3(codec);
			gifCodecCtx->codec_id = AV_CODEC_ID_GIF;
			gifCodecCtx->time_base = { 1, 30 };
			gifCodecCtx->bit_rate = 100000;
			gifCodecCtx->pix_fmt = AV_PIX_FMT_RGB8;
			gifCodecCtx->width = 600;
			gifCodecCtx->height = 400;
			ret = avcodec_open2(gifCodecCtx, codec, nullptr);
			ret = avcodec_parameters_from_context(outVideoStream->codecpar, gifCodecCtx);
		}
	}
	av_dict_copy(&outFmtCtx->metadata, inFmtCtx->metadata, 0);

	if (!(outFmtCtx->flags & AVFMT_NOFILE)) {
		ret = avio_open(&outFmtCtx->pb, output_filename, AVIO_FLAG_WRITE);
	}

	//写gif文件头
	ret = avformat_write_header(outFmtCtx, nullptr);

	int64_t first_pts = av_rescale_q_rnd((int64_t)from, AVRational{ 1,1 },
		inFmtCtx->streams[video_idx]->time_base,
		AV_ROUND_DOWN);
	av_seek_frame(inFmtCtx, video_idx, first_pts, AVSEEK_FLAG_BACKWARD);


	//通过滤镜缩放视频,修改图片像素格式
	VideoFilter *filter = nullptr;
	filter = new VideoFilter();
	char filter_descr[128];
	snprintf(filter_descr, sizeof(filter_descr), "scale=600:400,format=pix_fmts=%s",
		av_get_pix_fmt_name(gifCodecCtx->pix_fmt));
	VideoConfig in(videoCodecCtx->pix_fmt, videoCodecCtx->width, videoCodecCtx->height);
	VideoConfig out(gifCodecCtx->pix_fmt, gifCodecCtx->width, gifCodecCtx->height);
	filter->create(filter_descr, &in, &out);
	

	int gif_pts = 0;
	int index = 0;
	while (true) {
		AVPacket packet{ 0 };
		av_init_packet(&packet);
		ret = av_read_frame(inFmtCtx, &packet);
		if (ret < 0) 
		{
			break;
		}
		if (av_compare_ts(packet.pts, inFmtCtx->streams[packet.stream_index]->time_base,
			(int64_t)(to * 10), AVRational{ 1, 10 }) >= 0) {
			break;
		}

		//读取数据帧并进行输出
		if (packet.stream_index == video_idx) 
		{
			AVFrame *frame = av_frame_alloc();
			ret = avcodec_send_packet(videoCodecCtx, &packet);
			if (ret < 0) {
				continue;
			}
			ret = avcodec_receive_frame(videoCodecCtx, frame);
			if (ret < 0) {
				continue;
			}

			filter->filter(frame, frame);
			frame->pts = gif_pts++;

			ret = avcodec_send_frame(gifCodecCtx, frame);
			if (ret < 0) {
				continue;
			}
			av_frame_free(&frame);

			AVPacket gifPkt{ 0 };
			av_init_packet(&gifPkt);
			ret = avcodec_receive_packet(gifCodecCtx, &gifPkt);
			if (ret < 0)
			{
				continue;
			}
			gifPkt.stream_index = 0;
			av_packet_rescale_ts(&gifPkt, gifCodecCtx->time_base,
				outFmtCtx->streams[0]->time_base);

			ret = av_interleaved_write_frame(outFmtCtx, &gifPkt);
			
		}
	}
	
	if (filter != nullptr) {
		filter->destroy();
		delete filter;
	}

	avformat_close_input(&inFmtCtx);
	avformat_free_context(inFmtCtx);
	avformat_free_context(outFmtCtx);
	return 0;
}


int main(int argc, char* argv[])
{
	if (argc != 3)
	{
		printf("usage:%1 input filepath %2 outputfilepath");
		return -1;
	}
	//输入视频文件地址,输出gif的地址
	std::string fileInput = std::string(argv[1]);
	std::string fileOutput = std::string(argv[2]);
	

	avformat_network_init();
	convert_video_to_gif(fileOutput.c_str(), fileInput.c_str(), 30, 40);
}

3.gif优化

由于原始视频的像素比较高,帧率也比较高,这样截取出来的gif可能比较大。对于gif尺寸比较大,优化策略主要包括以下三点:
1.缩短截取的时间长度
2.对图片尺寸进行缩放处理
3.通过抽帧来降低视频的帧率

抽帧可能降低动图的流畅度,需要提起注意。

4.示例效果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

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

相关文章

Go sync.WaitGroup的学习

一.前言 二. 夯实基础 2.1 sync.WaitGroup是什么&#xff1f; Go语言中除了可以使用通道&#xff08;channel&#xff09;和互斥锁进行两个并发程序间的同步外&#xff0c;还可以使用等待组进行多个任务的同步&#xff0c;等待组可以保证在并发环境中完成指定数量的任务 在…

Spring Boot 中的Thymeleaf分页和排序示例

在上一篇文章中&#xff0c;我们已经知道如何构建Spring Boot Thymeleaf示例。今天&#xff0c;我将继续使用 Spring Data 和 Bootstrap 进行 Thymeleaf 分页和排序&#xff08;按列标题&#xff09;。 百里香叶分页和排序示例 假设我们在数据库中有这样的教程表&#xff1a;…

深入理解 Android 模块化里的资源冲突

翻译自 Understanding resource conflicts in Android ⚽ 前言 作为 Android 开发者&#xff0c;我们常常需要去管理非常多不同的资源文件&#xff0c;编译时这些资源文件会被统一地收集和整合到同一个包下面。根据官方的《Configure your build》文档介绍的构建过程可以总结这…

RFSoC应用笔记 - RF数据转换器 -22- API使用指南之配置DAC相关工作状态和中断相关函数使用

前言 本文完结后&#xff0c;关于RFSoC的配置的API函数部分就全部介绍完毕&#xff0c;后续有空将更新介绍简单的射频收发回环示例工程&#xff0c;不定时更新&#xff0c;敬请期待。 配置DAC相关工作状态 XRFdc_SetInterpolationFactor 函数原型 u32 XRFdc_SetInterpolat…

内存一致性,指令重排序,内存屏障,volatile解析

文章目录为什么会存在“内存可见性”问题重排序与内存可见性的关系as-if-serial语义单线程程序的重排序规则多线程程序的重排序规则happen-before是什么解决方案&#xff1a;内存屏障Volatile关键字解决内存可见性问题的实现原理为什么会存在“内存可见性”问题 下图为x86架构…

redis 的企业实战应用 (二)

前言&#xff1a; 如今redis的常用场景有 短信登录&#xff1a;使用redis共享session来实现 商户查询缓存&#xff1a;会理解缓存击穿&#xff0c;缓存穿透&#xff0c;缓存雪崩等问题&#xff0c;让小伙伴的对于这些概念的理解不仅仅是停留在概念上&#xff0c;更是能在代码…

【数学】仿射变换

∣降维打击NightguardSeries.∣\begin{vmatrix}\Huge{\textsf{ 降 维 打 击 }}\\\texttt{ Nightguard Series. }\end{vmatrix}∣∣∣∣∣​ 降 维 打 击 Nightguard Series. ​∣∣∣∣∣​ 注&#xff1a;本文讨论的仿射变换仅为y轴上的伸缩变换&#xff0c;且难度在高中生理…

H3CNE V7.0 视频教程

构建中小企业网络全套PPT汇总【V7版本】 第1章 计算机网络概述 第2章 OSI参考模型与TCP IP模型 第3章 局域网基本原理 第4章 广域网基本原理 第5章 IP基本原理 第6章 TCP和UDP基本原理 第7章 路由器、交换机及其操作系统介绍 第8章 命令行操作基础 第9章 网络设备文件…

mycat-3-实战篇

1 总结&#xff1a; 1&#xff1a;用的表必须在mycat的配置文件中配置。 2&#xff1a;mycat默认分片策略中&#xff0c;都是针对表的主键&#xff0c;默认是id,如果主键不是id的&#xff0c;请去rule.xml自己复制一份修改 3&#xff1a; 2 注意细讲解 1&#xff1a;schem…

Springboot启动流程分析(四):完成启动流程

目录 一 添加BeanPostProcessors到IOC容器 二 国际化支持 三 初始化监听器的多路播放器 四 刷新容器 五 注册监听器到IOC容器的多播器 六 完成bean的大规模实例化 6.1 大规模实例化bean 6.1.1 连续三层do...while循环作用 6.1.2 FactoryBean是什么&#xff1f;为什么要…

04 YAML kubetnetes世界里的通用语

文章目录1. 前言2. 声明式和命令式是怎么回事&#xff1f;3. 什么是YAML&#xff1f;4. 什么是API对象&#xff1f;4.1 k8s都有哪些资源对象4.2 列出kubectl 命令详细执行过程5. 如何描述 API 对象5.1 命令式5.2 声明式5.2.1 声明式YAML语法详解5.2.1.1 header部分详解5.2.1.2 …

【教学类-19-01】20221127《ABAB式-规律排序-A4竖版2份》(中班)

展示效果&#xff1a; 单人使用样式&#xff1a; 单页打印样式 ​ 背景需求&#xff1a; 中班幼儿需要掌握ABAB规律排序&#xff0c;如下图所示&#xff0c;AB两个元素能外形不同、颜色不同。 ​ ​利用Python Word单元格填色功能&#xff0c;随机生成AB样式&#xff0c;引…

STM32模拟IIC与IIC四种实现数字光强采集模块GY30(标准库与HAL库)

目录 代码实现是的IIC通信&#xff0c;数据采集后在串口显示&#xff0c;方便大家实现二次开发 原件选择 GY-30 数字光强度介绍 BH1750芯片参数 引脚说明 BH1750指令集 接线表设计 通过四种方式实现GY-30数据采集 1.标准库模拟IIC实现GY-30采集并串口1显示 2.标准库IIC…

重构uniapp uni-ui coloerUI项目

重构uniapp uni-ui coloerUI项目这里写自定义目录标题重构uniappuni-uicoloerUI项目起源流程重构uniappuni-uicoloerUI项目 起源 从网上复制了若依移动端的代码,但是对里面的文件夹布局方式和第三方组件库引入方式不甚了解,就想着从头创建一个空白项目&#xff0c;然后一步一…

Linux中设置开机启动执行命令和普通用户配置环境变量开机启动生效

记录&#xff1a;343 场景&#xff1a;在CentOS 7.9操作系统上&#xff0c;开机启动就执行自定义的命令&#xff0c;配置rc.local文件达到需求&#xff1b;在普通用户中配置环境变量开机启动生效&#xff0c;使用profile实现。 版本&#xff1a; 操作系统&#xff1a;CentOS…

01、Docker入门

目录 1、Docker是什么 2、Docker与虚拟化 3、Docker虚拟化的好处 好处一&#xff1a;应用部署方便 好处二&#xff1a;服务器同等配置&#xff0c;性能更优&#xff0c;利用率更高 4、核心概念 5、CentOS7 安装docker(在线方式) 6、镜像 7、Docker容器 8、查看Docker容…

typescript 八叉树的简单实现

查了一些文章&#xff0c;准备自己用typescript写一个简单的八叉树场景管理。 所谓的简单&#xff0c;就是所有元素都是边长为1的立方体。 元素类和树节点类 //元素类&#xff0c;因为都是边长为1的立方体&#xff0c;所以就用cube命名 export class CubeData {public reado…

由于没有远程桌面授权服务器可以提供许可证,远程会话连接已断开

一、问题描述 在使用Windows的远程桌面工具连接WindowsServer2016服务器时&#xff0c;无法连接到服务器&#xff0c;并且提示【由于没有远程桌面授权服务器可以提供许可证&#xff0c;远程回来连接已经断开。请跟服务器管理员联系】。 二、解决办法 2.0、前提 Windows Serv…

黑胶歌曲没权限,看我python大展神通,一分钟一个歌单

前言 大家早好、午好、晚好吖 ❤ ~ 人之初&#xff0c;喜白嫖。 大家都喜欢白嫖&#xff0c;我也喜欢&#xff0c;那么今天就来试试怎么白嫖抑云~ 一、需要的准备 1、环境 Python3.6以上 pycharm2019以上 2、模块 requests # 发送请求模块 第三方模块 exec js # 调用JS的…

CocosCreater 教程(下)

1.物理系统 1.1 2D刚体 刚体是组成物理世界的基本对象。 1.2 2D 碰撞组件 目前引擎支持三种不同的碰撞组件&#xff1a; 盒碰撞组件&#xff08;BoxCollider2D&#xff09;、圆形碰撞组件&#xff08;CircleCollider2D&#xff09; 和 多边形碰撞组件&#xff08;PolygonCo…