一、引入相关maven
二、根据小红书文章链接爬取文章内容和图片
三、根据图片、文字、音频等生成视频文件
1、生成视频工具类
2、上传视频到抖音
org.jsoup
jsoup
1.11.3
org.apache.commons
commons-lang3
3.4
org.bytedeco
javacv-platform
1.5.2
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import javax.servlet.http.HttpServletRequest;
/**
* 根据小红书链接爬取内容
* @param url
* @param request
* @return
* @throws Exception
*/
@RequestMapping("/analysis")
public JsonResp analysis(String url, HttpServletRequest request) throws Exception {
if (StringUtils.isBlank(url)) {
return JsonResp.toFail("小红书地址为空");
}
if (!url.contains("https://www.xiaohongshu.com") && !url.contains("http://xhslink.com")) {
return JsonResp.toFail("小红书地址不正确");
}
Connection connection = Jsoup.connect(url);
Connection data = connection.headers(getHeaderMap(request));
Document doc = data.get();
Map map = new HashMap<>();
//获取tag是title的所有dom文档
//标题
Elements elements = doc.getElementsByTag("title");
//获取第一个元素
Element element = elements.get(0);
//.html是返回html
String title = element.text();
map.put("title", title);
//图片
Elements elements1 = doc.select("span[class=inner]");
List list = new ArrayList<>();
for (Element element1 : elements1) {
String imageUrl = "http://" + element1.attr("style").replace("background-image:url(//", "").replace(");", "");
list.add(imageUrl);
}
map.put("list", list);
return JsonResp.ok(map);
}
/**
* 小红书请求头设置
*/
public static Map getHeaderMap(HttpServletRequest request) {
String ip = getIp(request);
Map map = new HashMap<>(new LinkedHashMap<>());
if (StringUtils.isNotEmpty(ip) && !ip.contains("127.0.0.1") && !ip.contains("192.168")) {
map.put("Accept", "text/html, application/xhtml+xml, image/jxr, */*");
map.put("Accept-Encoding", "gzip, deflate");
map.put("x-forwarded-for", ip);
map.put("Proxy-Client-IP", ip);
map.put("WL-Proxy-Client-IP", ip);
map.put("HTTP_CLIENT_IP", ip);
map.put("HTTP_X_FORWARDED_FOR", ip);
}
map.put("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9");
map.put("accept-encoding", "gzip, deflate, br");
map.put("accept-language", "zh-CN,zh;q=0.9,en;q=0.8");
map.put("cache-control", "max-age=0");
map.put("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36");
return map;
}
/**
* 获取客户端ip
*
* @param httpServletRequest
* @return
*/
public static String getIp(HttpServletRequest httpServletRequest) {
String ip = httpServletRequest.getHeader("x-forwarded-for");
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = httpServletRequest.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = httpServletRequest.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = httpServletRequest.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = httpServletRequest.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = httpServletRequest.getRemoteAddr();
}
ip = "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip.split(",")[0];
return ip;
}
JsonResp 是项目里公共的接口返回类型 这里可以直接忽略
import org.apache.commons.lang3.StringUtils;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.*;
import org.bytedeco.javacv.Frame;
import org.bytedeco.opencv.global.opencv_core;
import org.bytedeco.opencv.opencv_core.IplImage;
import sun.font.FontDesignMetrics;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import static org.bytedeco.opencv.helper.opencv_imgcodecs.cvLoadImage;
/**
* 视频生成工具类
*
* @author fengzi
* @version 1.0
* @date 2020/07/03
*/
public class GenerateVideo {
/**
* 图片合成视频
*
* @param picturesPath 图片文件夹路径
* @param videoPath 视频存放地址
* @param second 每隔second秒切换一张图
* @param audioPath 音频地址
* @param cover 视频封面 选填
* @param title 封面标题 选填
* @param color 标题颜色 选填
* @param width 视频宽度
* @param height 视频高度
* @param audioStart 音频开始位置 秒 选题
* @throws FrameRecorder.Exception Exception
*/
public static void createMp4(String picturesPath, String videoPath, int second, String audioPath, File cover, String title, Color color, Integer width, Integer height, Integer audioStart) throws FrameRecorder.Exception {
FFmpegFrameRecorder recorder = null;
// int width = 1080;
// int height = 1440;
if (width == null) {
width = 1080;
}
if (height == null) {
height = 1440;
}
try {
int frameRate = 25;
//读取所有图片
File file = new File(picturesPath);
File[] files = file.listFiles();
if (files == null) {
files = new File[]{};
}
List fileList = Arrays.stream(files).sorted(Comparator.comparing(File::getPath)).collect(Collectors.toList());
if (cover != null) {
fileList.add(0, cover);
}
//视频宽高最好是按照常见的视频的宽高 16:9 或者 9:16
recorder = new FFmpegFrameRecorder(videoPath, width, height);
//设置视频编码层模式
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
//设置视频为25帧每秒
recorder.setFrameRate(frameRate);
//设置视频图像数据格式
recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
recorder.setFormat("mp4");
// 先默认吧,这个应该属于设置视频的处理模式 不可变(固定)音频比特率
recorder.setAudioOption("crf", "0");
// 最高质量
recorder.setAudioQuality(0);
// 音频比特率
recorder.setAudioBitrate(192000);
// 音频采样率
recorder.setSampleRate(44100);
// 双通道(立体声)
recorder.setAudioChannels(2);
// 音频编/解码器
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
// 关键帧间隔,一般与帧率相同或者是视频帧率的两倍
recorder.setGopSize(frameRate * 2);
recorder.start();
Frame frameImage;
OpenCVFrameConverter.ToIplImage conveter = new OpenCVFrameConverter.ToIplImage();
Java2DFrameConverter converter = new Java2DFrameConverter();
int key = 0;
for (File f : fileList) {
String fName = f.getPath();
if (!fName.contains(".png") && !fName.contains(".jpg")) {
continue;
}
///图片尺寸调整
IplImage image = cvLoadImage(fName);
frameImage = conveter.convert(image);
BufferedImage read = converter.convert(frameImage);
int imageType = Java2DFrameConverter.getBufferedImageType(frameImage);
read = reduceImageScale(read, width, height, imageType);
if (key == 0 && StringUtils.isNotEmpty(title)) {
markText(read, title, color);
}
key++;
for (int j = 0; j < frameRate * second; j++) {
recorder.record(converter.convert(read));
}
opencv_core.cvReleaseImage(image);
}
if (StringUtils.isEmpty(audioPath)) {
return;
}
System.err.println("------开始录制音频------");
FrameGrabber audioFrames = new FFmpegFrameGrabber(audioPath);
audioFrames.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
audioFrames.start();
//音频开始前的读取出来抛弃掉
if (audioStart != null && audioStart > 0) {
for (int j = 0; j <= frameRate * audioStart + frameRate * second; j++) {
audioFrames.grab();
}
}
Frame frameAudio;
//一秒是25帧 所以要记录25次
for (int j = 0; j < frameRate * second * key * 1.1; j++) {
frameAudio = audioFrames.grab();
recorder.record(frameAudio);
}
audioFrames.stop();
audioFrames.release();
System.err.println("------结束录制音频------");
} catch (Exception e) {
e.printStackTrace();
} finally {
//最后一定要结束并释放资源
if (recorder != null) {
recorder.stop();
recorder.release();
}
}
}
/**
* 填充图片为png格式,填充部分为透明色
*
* @param srcImage 源文件
* @param destWidth 设置图片宽度
* @param destHeight 设置图片高度
* @return BufferedImage
*/
public static BufferedImage reduceImageScale(final BufferedImage srcImage, int destWidth, int destHeight, int imageType) {
int oldheight = srcImage.getHeight();
int oldwidth = srcImage.getWidth();
if (oldheight == destHeight && oldwidth == destWidth) {
return srcImage;
}
BufferedImage outImage = null;
try {
BigDecimal rate = new BigDecimal(destWidth).divide(new BigDecimal(destHeight), 2, BigDecimal.ROUND_HALF_DOWN);
BigDecimal oldRate = new BigDecimal(oldwidth).divide(new BigDecimal(oldheight), 2, BigDecimal.ROUND_HALF_DOWN);
//新比例比旧的比例高 以高为准
if (rate.compareTo(oldRate) >= 0) {
if (oldheight > destHeight) {
destHeight = oldheight;
destWidth = new BigDecimal(oldheight).multiply(rate).intValue();
}
} else {
//新比例比旧的比例低 以宽为准
if (oldwidth > destWidth) {
destWidth = oldwidth;
destHeight = new BigDecimal(oldwidth).divide(rate, 0, BigDecimal.ROUND_HALF_DOWN).intValue();
}
}
System.err.println(destWidth + "----" + destHeight);
outImage = new BufferedImage(destWidth, destHeight, imageType);
Graphics2D graphics2D = outImage.createGraphics();
outImage = graphics2D.getDeviceConfiguration().createCompatibleImage(destWidth, destHeight, Transparency.OPAQUE);
graphics2D.dispose();
graphics2D = outImage.createGraphics();
// 设置图片居中显示
graphics2D.drawImage(srcImage, (destWidth - oldwidth) / 2,
(destHeight - oldheight) / 2, null);
return outImage;
} catch (Exception e) {
e.printStackTrace();
}
return srcImage;
}
/**
* @param bufImg 图片
* @param text 最多12个字
* @param color 颜色
* @throws Exception 异常
*/
public static void markText(BufferedImage bufImg, String text, Color color) throws Exception {
text = filterEmoji(text);
if (text.length() > 15) {
text = text.substring(0, 15);
}
Font font = new Font("宋体", Font.BOLD, 65);
Graphics2D g = bufImg.createGraphics();
g.drawImage(bufImg, bufImg.getWidth(), bufImg.getHeight(), null);
g.setColor(color);
g.setFont(font);
int srcImgWidth = bufImg.getWidth();
int srcImgHeight = bufImg.getHeight();
FontDesignMetrics metrics = FontDesignMetrics.getMetrics(font);
int fontWidth = metrics.charsWidth(text.toCharArray(), 0, text.length());
int fontHeight = metrics.getHeight();
System.out.println("fontWidth----" + fontWidth + ",fontHeight----" + fontHeight);
int x = 100;
if (fontWidth < srcImgWidth) {
x = (srcImgWidth - fontWidth) / 2;
}
g.drawString(text, x, (srcImgHeight + fontHeight) / 2 - 15);
g.dispose();
}
/**
* 过滤emoji 或者 其他非文字类型的字符
*
* @param source
* @return
*/
public static String filterEmoji(String source) {
if (StringUtils.isBlank(source)) {
return source;
}
StringBuilder buf = null;
int len = source.length();
for (int i = 0; i < len; i++) {
char codePoint = source.charAt(i);
if (isEmojiCharacter(codePoint)) {
if (buf == null) {
buf = new StringBuilder(source.length());
}
buf.append(codePoint);
}
}
if (buf == null) {
return source;
} else {
if (buf.length() == len) {
buf = null;
return source;
} else {
return buf.toString();
}
}
}
private static boolean isEmojiCharacter(char codePoint) {
return (codePoint == 0x0) || (codePoint == 0x9) || (codePoint == 0xA)
|| (codePoint == 0xD)
|| ((codePoint >= 0x20) && (codePoint <= 0xD7FF))
|| ((codePoint >= 0xE000) && (codePoint <= 0xFFFD))
|| ((codePoint >= 0x10000) && (codePoint <= 0x10FFFF));
}
}
videoPath 为视频生成后的地址
该方法可实现图片+文字+音频合成视频
可自定义文本颜色、音频开始位置、图片切换间隔、不同图片尺寸默认上下左右居中不变形
具体参考 https://open.douyin.com 抖音开放平台
主要对接以下功能:
1、账号授权:https://open.douyin.com/platform/doc/6848834666171009035 获取授权码
2、上传视频:https://open.douyin.com/platform/doc/6848798087398295555 获取video_id 下一步创建视频会用到
3、创建视频:https://open.douyin.com/platform/doc/6848798087398328323 创建成功会返回抖音视频id item_id
其他的接口如查询已发布的视频列表 删除视频等可自行查看抖音相关文档