当前位置:   article > 正文

Java SpringBoot Jsoup爬取小红书文章内容 利用JavaCV自动生成音视频 并发布到抖音_如何将视频通过接口上传到抖音平台 java

如何将视频通过接口上传到抖音平台 java

一、引入相关maven

二、根据小红书文章链接爬取文章内容和图片

三、根据图片、文字、音频等生成视频文件

1、生成视频工具类

2、上传视频到抖音


一、引入相关maven

  1. <!-- Jsoup 解析HTML文本 -->
  2. <dependency>
  3. <groupId>org.jsoup</groupId>
  4. <artifactId>jsoup</artifactId>
  5. <version>1.11.3</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.apache.commons</groupId>
  9. <artifactId>commons-lang3</artifactId>
  10. <version>3.4</version>
  11. </dependency>
  12. <!-- 主要用JavaCV来处理视频生成 -->
  13. <dependency>
  14. <groupId>org.bytedeco</groupId>
  15. <artifactId>javacv-platform</artifactId>
  16. <version>1.5.2</version>
  17. </dependency>

二、根据小红书文章链接爬取文章内容和图片

  1. import org.jsoup.Connection;
  2. import org.jsoup.Jsoup;
  3. import org.jsoup.nodes.Document;
  4. import org.jsoup.nodes.Element;
  5. import org.jsoup.select.Elements;
  6. import javax.servlet.http.HttpServletRequest;
  7. /**
  8. * 根据小红书链接爬取内容
  9. * @param url
  10. * @param request
  11. * @return
  12. * @throws Exception
  13. */
  14. @RequestMapping("/analysis")
  15. public JsonResp analysis(String url, HttpServletRequest request) throws Exception {
  16. if (StringUtils.isBlank(url)) {
  17. return JsonResp.toFail("小红书地址为空");
  18. }
  19. if (!url.contains("https://www.xiaohongshu.com") && !url.contains("http://xhslink.com")) {
  20. return JsonResp.toFail("小红书地址不正确");
  21. }
  22. Connection connection = Jsoup.connect(url);
  23. Connection data = connection.headers(getHeaderMap(request));
  24. Document doc = data.get();
  25. Map<String, Object> map = new HashMap<>();
  26. //获取tag是title的所有dom文档
  27. //标题
  28. Elements elements = doc.getElementsByTag("title");
  29. //获取第一个元素
  30. Element element = elements.get(0);
  31. //.html是返回html
  32. String title = element.text();
  33. map.put("title", title);
  34. //图片
  35. Elements elements1 = doc.select("span[class=inner]");
  36. List<String> list = new ArrayList<>();
  37. for (Element element1 : elements1) {
  38. String imageUrl = "http://" + element1.attr("style").replace("background-image:url(//", "").replace(");", "");
  39. list.add(imageUrl);
  40. }
  41. map.put("list", list);
  42. return JsonResp.ok(map);
  43. }
  44. /**
  45. * 小红书请求头设置
  46. */
  47. public static Map<String, String> getHeaderMap(HttpServletRequest request) {
  48. String ip = getIp(request);
  49. Map<String, String> map = new HashMap<>(new LinkedHashMap<>());
  50. if (StringUtils.isNotEmpty(ip) && !ip.contains("127.0.0.1") && !ip.contains("192.168")) {
  51. map.put("Accept", "text/html, application/xhtml+xml, image/jxr, */*");
  52. map.put("Accept-Encoding", "gzip, deflate");
  53. map.put("x-forwarded-for", ip);
  54. map.put("Proxy-Client-IP", ip);
  55. map.put("WL-Proxy-Client-IP", ip);
  56. map.put("HTTP_CLIENT_IP", ip);
  57. map.put("HTTP_X_FORWARDED_FOR", ip);
  58. }
  59. 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");
  60. map.put("accept-encoding", "gzip, deflate, br");
  61. map.put("accept-language", "zh-CN,zh;q=0.9,en;q=0.8");
  62. map.put("cache-control", "max-age=0");
  63. 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");
  64. return map;
  65. }
  66. /**
  67. * 获取客户端ip
  68. *
  69. * @param httpServletRequest
  70. * @return
  71. */
  72. public static String getIp(HttpServletRequest httpServletRequest) {
  73. String ip = httpServletRequest.getHeader("x-forwarded-for");
  74. if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
  75. ip = httpServletRequest.getHeader("Proxy-Client-IP");
  76. }
  77. if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
  78. ip = httpServletRequest.getHeader("WL-Proxy-Client-IP");
  79. }
  80. if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
  81. ip = httpServletRequest.getHeader("HTTP_CLIENT_IP");
  82. }
  83. if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
  84. ip = httpServletRequest.getHeader("HTTP_X_FORWARDED_FOR");
  85. }
  86. if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
  87. ip = httpServletRequest.getRemoteAddr();
  88. }
  89. ip = "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip.split(",")[0];
  90. return ip;
  91. }

JsonResp 是项目里公共的接口返回类型 这里可以直接忽略

三、根据图片、文字、音频等生成视频文件

1、生成视频工具类

  1. import org.apache.commons.lang3.StringUtils;
  2. import org.bytedeco.ffmpeg.global.avcodec;
  3. import org.bytedeco.ffmpeg.global.avutil;
  4. import org.bytedeco.javacv.*;
  5. import org.bytedeco.javacv.Frame;
  6. import org.bytedeco.opencv.global.opencv_core;
  7. import org.bytedeco.opencv.opencv_core.IplImage;
  8. import sun.font.FontDesignMetrics;
  9. import java.awt.*;
  10. import java.awt.image.BufferedImage;
  11. import java.io.File;
  12. import java.math.BigDecimal;
  13. import java.util.Arrays;
  14. import java.util.Comparator;
  15. import java.util.List;
  16. import java.util.stream.Collectors;
  17. import static org.bytedeco.opencv.helper.opencv_imgcodecs.cvLoadImage;
  18. /**
  19. * 视频生成工具类
  20. *
  21. * @author fengzi
  22. * @version 1.0
  23. * @date 2020/07/03
  24. */
  25. public class GenerateVideo {
  26. /**
  27. * 图片合成视频
  28. *
  29. * @param picturesPath 图片文件夹路径
  30. * @param videoPath 视频存放地址
  31. * @param second 每隔second秒切换一张图
  32. * @param audioPath 音频地址
  33. * @param cover 视频封面 选填
  34. * @param title 封面标题 选填
  35. * @param color 标题颜色 选填
  36. * @param width 视频宽度
  37. * @param height 视频高度
  38. * @param audioStart 音频开始位置 秒 选题
  39. * @throws FrameRecorder.Exception Exception
  40. */
  41. 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 {
  42. FFmpegFrameRecorder recorder = null;
  43. // int width = 1080;
  44. // int height = 1440;
  45. if (width == null) {
  46. width = 1080;
  47. }
  48. if (height == null) {
  49. height = 1440;
  50. }
  51. try {
  52. int frameRate = 25;
  53. //读取所有图片
  54. File file = new File(picturesPath);
  55. File[] files = file.listFiles();
  56. if (files == null) {
  57. files = new File[]{};
  58. }
  59. List<File> fileList = Arrays.stream(files).sorted(Comparator.comparing(File::getPath)).collect(Collectors.toList());
  60. if (cover != null) {
  61. fileList.add(0, cover);
  62. }
  63. //视频宽高最好是按照常见的视频的宽高 16:9 或者 9:16
  64. recorder = new FFmpegFrameRecorder(videoPath, width, height);
  65. //设置视频编码层模式
  66. recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
  67. //设置视频为25帧每秒
  68. recorder.setFrameRate(frameRate);
  69. //设置视频图像数据格式
  70. recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
  71. recorder.setFormat("mp4");
  72. // 先默认吧,这个应该属于设置视频的处理模式 不可变(固定)音频比特率
  73. recorder.setAudioOption("crf", "0");
  74. // 最高质量
  75. recorder.setAudioQuality(0);
  76. // 音频比特率
  77. recorder.setAudioBitrate(192000);
  78. // 音频采样率
  79. recorder.setSampleRate(44100);
  80. // 双通道(立体声)
  81. recorder.setAudioChannels(2);
  82. // 音频编/解码器
  83. recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
  84. // 关键帧间隔,一般与帧率相同或者是视频帧率的两倍
  85. recorder.setGopSize(frameRate * 2);
  86. recorder.start();
  87. Frame frameImage;
  88. OpenCVFrameConverter.ToIplImage conveter = new OpenCVFrameConverter.ToIplImage();
  89. Java2DFrameConverter converter = new Java2DFrameConverter();
  90. int key = 0;
  91. for (File f : fileList) {
  92. String fName = f.getPath();
  93. if (!fName.contains(".png") && !fName.contains(".jpg")) {
  94. continue;
  95. }
  96. ///图片尺寸调整
  97. IplImage image = cvLoadImage(fName);
  98. frameImage = conveter.convert(image);
  99. BufferedImage read = converter.convert(frameImage);
  100. int imageType = Java2DFrameConverter.getBufferedImageType(frameImage);
  101. read = reduceImageScale(read, width, height, imageType);
  102. if (key == 0 && StringUtils.isNotEmpty(title)) {
  103. markText(read, title, color);
  104. }
  105. key++;
  106. for (int j = 0; j < frameRate * second; j++) {
  107. recorder.record(converter.convert(read));
  108. }
  109. opencv_core.cvReleaseImage(image);
  110. }
  111. if (StringUtils.isEmpty(audioPath)) {
  112. return;
  113. }
  114. System.err.println("------开始录制音频------");
  115. FrameGrabber audioFrames = new FFmpegFrameGrabber(audioPath);
  116. audioFrames.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
  117. audioFrames.start();
  118. //音频开始前的读取出来抛弃掉
  119. if (audioStart != null && audioStart > 0) {
  120. for (int j = 0; j <= frameRate * audioStart + frameRate * second; j++) {
  121. audioFrames.grab();
  122. }
  123. }
  124. Frame frameAudio;
  125. //一秒是25帧 所以要记录25次
  126. for (int j = 0; j < frameRate * second * key * 1.1; j++) {
  127. frameAudio = audioFrames.grab();
  128. recorder.record(frameAudio);
  129. }
  130. audioFrames.stop();
  131. audioFrames.release();
  132. System.err.println("------结束录制音频------");
  133. } catch (Exception e) {
  134. e.printStackTrace();
  135. } finally {
  136. //最后一定要结束并释放资源
  137. if (recorder != null) {
  138. recorder.stop();
  139. recorder.release();
  140. }
  141. }
  142. }
  143. /**
  144. * 填充图片为png格式,填充部分为透明色
  145. *
  146. * @param srcImage 源文件
  147. * @param destWidth  设置图片宽度
  148. * @param destHeight  设置图片高度
  149. * @return BufferedImage
  150. */
  151. public static BufferedImage reduceImageScale(final BufferedImage srcImage, int destWidth, int destHeight, int imageType) {
  152. int oldheight = srcImage.getHeight();
  153. int oldwidth = srcImage.getWidth();
  154. if (oldheight == destHeight && oldwidth == destWidth) {
  155. return srcImage;
  156. }
  157. BufferedImage outImage = null;
  158. try {
  159. BigDecimal rate = new BigDecimal(destWidth).divide(new BigDecimal(destHeight), 2, BigDecimal.ROUND_HALF_DOWN);
  160. BigDecimal oldRate = new BigDecimal(oldwidth).divide(new BigDecimal(oldheight), 2, BigDecimal.ROUND_HALF_DOWN);
  161. //新比例比旧的比例高 以高为准
  162. if (rate.compareTo(oldRate) >= 0) {
  163. if (oldheight > destHeight) {
  164. destHeight = oldheight;
  165. destWidth = new BigDecimal(oldheight).multiply(rate).intValue();
  166. }
  167. } else {
  168. //新比例比旧的比例低 以宽为准
  169. if (oldwidth > destWidth) {
  170. destWidth = oldwidth;
  171. destHeight = new BigDecimal(oldwidth).divide(rate, 0, BigDecimal.ROUND_HALF_DOWN).intValue();
  172. }
  173. }
  174. System.err.println(destWidth + "----" + destHeight);
  175. outImage = new BufferedImage(destWidth, destHeight, imageType);
  176. Graphics2D graphics2D = outImage.createGraphics();
  177. outImage = graphics2D.getDeviceConfiguration().createCompatibleImage(destWidth, destHeight, Transparency.OPAQUE);
  178. graphics2D.dispose();
  179. graphics2D = outImage.createGraphics();
  180. // 设置图片居中显示
  181. graphics2D.drawImage(srcImage, (destWidth - oldwidth) / 2,
  182. (destHeight - oldheight) / 2, null);
  183. return outImage;
  184. } catch (Exception e) {
  185. e.printStackTrace();
  186. }
  187. return srcImage;
  188. }
  189. /**
  190. * @param bufImg 图片
  191. * @param text 最多12个字
  192. * @param color 颜色
  193. * @throws Exception 异常
  194. */
  195. public static void markText(BufferedImage bufImg, String text, Color color) throws Exception {
  196. text = filterEmoji(text);
  197. if (text.length() > 15) {
  198. text = text.substring(0, 15);
  199. }
  200. Font font = new Font("宋体", Font.BOLD, 65);
  201. Graphics2D g = bufImg.createGraphics();
  202. g.drawImage(bufImg, bufImg.getWidth(), bufImg.getHeight(), null);
  203. g.setColor(color);
  204. g.setFont(font);
  205. int srcImgWidth = bufImg.getWidth();
  206. int srcImgHeight = bufImg.getHeight();
  207. FontDesignMetrics metrics = FontDesignMetrics.getMetrics(font);
  208. int fontWidth = metrics.charsWidth(text.toCharArray(), 0, text.length());
  209. int fontHeight = metrics.getHeight();
  210. System.out.println("fontWidth----" + fontWidth + ",fontHeight----" + fontHeight);
  211. int x = 100;
  212. if (fontWidth < srcImgWidth) {
  213. x = (srcImgWidth - fontWidth) / 2;
  214. }
  215. g.drawString(text, x, (srcImgHeight + fontHeight) / 2 - 15);
  216. g.dispose();
  217. }
  218. /**
  219. * 过滤emoji 或者 其他非文字类型的字符
  220. *
  221. * @param source
  222. * @return
  223. */
  224. public static String filterEmoji(String source) {
  225. if (StringUtils.isBlank(source)) {
  226. return source;
  227. }
  228. StringBuilder buf = null;
  229. int len = source.length();
  230. for (int i = 0; i < len; i++) {
  231. char codePoint = source.charAt(i);
  232. if (isEmojiCharacter(codePoint)) {
  233. if (buf == null) {
  234. buf = new StringBuilder(source.length());
  235. }
  236. buf.append(codePoint);
  237. }
  238. }
  239. if (buf == null) {
  240. return source;
  241. } else {
  242. if (buf.length() == len) {
  243. buf = null;
  244. return source;
  245. } else {
  246. return buf.toString();
  247. }
  248. }
  249. }
  250. private static boolean isEmojiCharacter(char codePoint) {
  251. return (codePoint == 0x0) || (codePoint == 0x9) || (codePoint == 0xA)
  252. || (codePoint == 0xD)
  253. || ((codePoint >= 0x20) && (codePoint <= 0xD7FF))
  254. || ((codePoint >= 0xE000) && (codePoint <= 0xFFFD))
  255. || ((codePoint >= 0x10000) && (codePoint <= 0x10FFFF));
  256. }
  257. }

videoPath 为视频生成后的地址

该方法可实现图片+文字+音频合成视频

可自定义文本颜色、音频开始位置、图片切换间隔、不同图片尺寸默认上下左右居中不变形

2、上传视频到抖音

具体参考 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 

其他的接口如查询已发布的视频列表 删除视频等可自行查看抖音相关文档

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/90527
推荐阅读
相关标签
  

闽ICP备14008679号