阅读 111

java 采用腾讯云直播实现多方视频录制并每路画面添加相应的水印

java 采用腾讯云直播实现多方视频录制并每路画面添加相应的水印

这是我第一篇文章,本人也是菜鸟,如果有什么不对,也请大神多多指点

话不多说,进入正题。


首先录制视频的前提是推流和拉流同时是连接上才能进行录制工作。否则腾讯云不会给你返回录制的视频地址。


如果你不知道拉流和推流是什么,可以参考腾讯云的api


https://cloud.tencent.com/document/product/267/13551


说到水印就需要有一个生成水印的方法,我的水印是根据字符串动态生成的


package cn.meefo.common.util;


import javax.imageio.ImageIO;

import java.awt.*;

import java.awt.font.FontRenderContext;

import java.awt.geom.Rectangle2D;

import java.awt.image.BufferedImage;

import java.io.File;

import java.io.IOException;

import java.io.OutputStream;

import java.util.UUID;


/**

 * 创建文字图片

 * @author yuki_ho

 *

 */

public class FontImage {

    // 默认格式

    private static final String FORMAT_NAME = "JPG";

    // 默认 宽度

    private static final int WIDTH = 100;

    // 默认 高度

    private static final int HEIGHT =100;


    /**

     * 创建图片

     * @param content 内容

     * @param font  字体

     * @param width 宽

     * @param height 高

     * @return

     */

    private static BufferedImage createImage(String content,Font font,Integer width,Integer height) throws IOException {

        BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        Graphics2D g2 = (Graphics2D)bi.getGraphics();

        bi =g2.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);

        g2.dispose();

        g2=bi.createGraphics();

//        BufferedImage image2 = ImageIO.read(new FileInputStream(new File("C:\\apache-tomcat-9.0.13_t\\webapps\\nb_jcy_t\\static\\images\\background\\background.png")));

        g2.setBackground(Color.WHITE);

//        g2.drawImage(image2, 0, 0, 80, 80,null);

        g2.clearRect(0, 0, width, height);

        g2.setPaint(Color.BLACK);


        FontRenderContext context = g2.getFontRenderContext();

        Rectangle2D bounds = font.getStringBounds(content, context);

        double x = (width - bounds.getWidth()) / 2;

        double y = (height - bounds.getHeight()) / 2;

        double ascent = -bounds.getY();

        double baseY = y + ascent;


        g2.drawString(content, (int)x, (int)baseY);


        return bi;

    }


    /**

     * 获取 图片

     * @param content 内容

     * @param font  字体

     * @param width 宽

     * @param height 高

     * @return

     */

    public static BufferedImage getImage(String content,Font font,Integer width,Integer height) throws IOException {

        width=width==null?WIDTH:width;

        height=height==null?HEIGHT:height;

        if(null==font)

            font = new Font("宋体", Font.BOLD, 11);

        return createImage(content, font, width, height);

    }


    /**

     * 获取 图片

     * @param content 内容

     * @param width 宽

     * @param height 高

     * @return

     */

    public static BufferedImage getImage(String content,Integer width,Integer height) throws IOException {


        return getImage(content, null,width, height);

    }


    /**

     * 获取图片

     * @param content 内容

     * @return

     */

    public static BufferedImage getImage(String content) throws IOException {


        return getImage(content, null, null);

    }


    /**

     *  获取图片

     * @param content 内容

     * @param font 字体

     * @param width 宽

     * @param height 高

     * @param destPath 输出路径

     * @throws IOException

     */

    public static String getImage(String content,Font font ,Integer width,Integer height,String destPath) throws IOException{

        mkdirs(destPath);

        String file = UUID.randomUUID().toString()+".jpg";

        ImageIO.write(getImage(content,font,width,height),FORMAT_NAME, new File(destPath+"/"+file));

        return "https://**********/static/images/watermark/"+file;   //返回的是线上图片的地址

    }


    /**

     * 获取图片

     * @param content 内容

     * @param font 字体

     * @param width 宽

     * @param height 高

     * @param output 输出流

     * @throws IOException

     */

    public static void getImage(String content,Font font,Integer width,Integer height, OutputStream output) throws IOException{

        ImageIO.write(getImage(content,font,width,height), FORMAT_NAME, output);

    }


    /**

     * 获取图片

     * @param content 内容

     * @param width 宽

     * @param height 高

     * @param destPath 输出路径

     * @throws IOException

     */

    public static String getImage(String content,Integer width,Integer height,String destPath) throws IOException{

        String image = getImage(content, null, width, height, destPath);

        return image;

    }


    /**

     * 获取图片

     * @param content 内容

     * @param width 宽

     * @param height 高

     * @param output 输出流

     * @throws IOException

     */

    public static void getImage(String content,Integer width,Integer height, OutputStream output) throws IOException{

        getImage(content, null, width, height, output);

    }



    /**

     * 创建 目录

     * @param destPath

     */

    public static void mkdirs(String destPath) {

        File file =new File(destPath);

        //当文件夹不存在时,mkdirs会自动创建多层目录,区别于mkdir.(mkdir如果父目录不存在则会抛出异常)

        if (!file.exists() && !file.isDirectory()) {

            file.mkdirs();

        }

    }


    public static void main(String[] args) throws Exception {

        String path = getImage("MAS-123456", 100, 100, "d:/test");


    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

然后就是根据水印的地址转换为腾讯云的水印id

我写了一个TencentLiveUtil的工具类,其实里面有很多方法,我只展示需要的代码

注意 SecretId 和 SecretKey 需填写自己申请的腾讯云的SecretId 和 SecretKey


package cn.meefo.common.util;


import com.tencentcloudapi.common.Credential;

import com.tencentcloudapi.common.exception.TencentCloudSDKException;

import com.tencentcloudapi.common.profile.ClientProfile;

import com.tencentcloudapi.common.profile.HttpProfile;

import com.tencentcloudapi.live.v20180801.LiveClient;

import com.tencentcloudapi.live.v20180801.models.AddLiveWatermarkRequest;

import com.tencentcloudapi.live.v20180801.models.AddLiveWatermarkResponse;

import net.sf.json.JSONArray;

import net.sf.json.JSONObject;

import org.apache.commons.codec.binary.Base64;


import javax.crypto.Mac;

import javax.crypto.spec.SecretKeySpec;

import javax.xml.bind.DatatypeConverter;

import java.io.UnsupportedEncodingException;

import java.net.URLEncoder;

import java.security.InvalidKeyException;

import java.security.MessageDigest;

import java.security.NoSuchAlgorithmException;

import java.text.ParseException;

import java.text.SimpleDateFormat;

import java.util.*;


public class TencentLiveUtil {

    //添加水印

public static int addLiveWatermark(String pictureUrl,String XPosition,String YPosition){

String SecretId = SECRETID;

String Action = "AddLiveWatermark";

String Region ="ap-shanghai";//区域参数

String SecretKey  = SECRETKEY; 

try{

Credential cred = new Credential(SecretId, SecretKey);

HttpProfile httpProfile = new HttpProfile();

httpProfile.setEndpoint("live.tencentcloudapi.com");

ClientProfile clientProfile = new ClientProfile();

clientProfile.setHttpProfile(httpProfile);


LiveClient client = new LiveClient(cred, "ap-shanghai", clientProfile);

String params ="{\"PictureUrl\":"+pictureUrl+",\"WatermarkName\":\"log\"}";

System.out.println("pictureUrl==="+pictureUrl);

String params = "{\"PictureUrl\":\"" +pictureUrl+ "\",\"WatermarkName\":\"log\",\"XPosition\":\"" +XPosition+ "\",\"YPosition\":\"" +YPosition+ "\"}";

AddLiveWatermarkRequest req = AddLiveWatermarkRequest.fromJsonString(params, AddLiveWatermarkRequest.class);

AddLiveWatermarkResponse resp = client.AddLiveWatermark(req);

String watermark = AddLiveWatermarkRequest.toJsonString(resp);

System.out.println(AddLiveWatermarkRequest.toJsonString(resp));

JSONObject jsonObject = JSONObject.fromObject(watermark);

int watermarkId = (int)jsonObject.get("WatermarkId");

return watermarkId;   

} catch (TencentCloudSDKException e) {

System.out.println(e.toString());

return 0;

}

}

}


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

这样我就获得了watermarkId这个水印id

那我还要知道哪个人的流(推流/拉流)对应哪个水印啊

所以我将他们的流id(stream_id)改成身份证号 (这是我的业务逻辑,因为身份证号是唯一的,不同的业务逻辑可以用不同的方法)

然后将身份证作为key ,图片作为value传入map中


而水印id我是提前在录制视频前调用接口生成,就是在页面加载的时候调用的该接口


  //生成水印地址

    @RequestMapping({"/makeWatermark"})

    @ResponseBody

    public JSONObject makeWatermark(String idsArr, String caseId, String roomId){

        JSONObject jsonObject = new JSONObject();

        String[] idCards = idsArr.split(",");

        Map<String,Object>map=new HashMap<>();

        for(int i=0;i<idCards.length;i++){

            String roleName = getRoleName(caseId, roomId, idCards[i]);

            String filePath = null;

            try {

                //生成水印并返回地址

                //注意返回的项目域名已经写死

                filePath = FontImage.getImage(roleName, 80, 80, "********");//图片存放的目录根据自己的项目传参

              int result = TencentLiveUtil.addLiveWatermark(filePath, "10", "10");

                map.put(idCards[i],result );//map的key为该人员的身份证,以及该人员的水印id

            } catch (IOException e) {

                e.printStackTrace();

            }


        }

        jsonObject.put("data",map);

        return jsonObject;

    }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

然后就是做一个录制按钮,来调用录制视频的接口,其中pushUrl是推流的streamid ,playerArr是拉流的streamid数组,为了业务逻辑,我将他们的值定为了身份证号码


//录制视频

    @RequestMapping({"/recordingVideo"})

    @ResponseBody

    public JSONObject recordingVideo(String pushUrl, String caseId, String roomId, String playerArr, String idCards,String map1) throws IOException, InterruptedException {

        JSONObject jsonObject = new JSONObject();

        String s = "";

        String[] playerUrls = playerArr.split(",");  //前端传回来的拉流数组,我的业务里面拉流不固定,我就主要说说两路画面(一个推流,一个拉流)

        String output_stream_id = (new GUID2()).toString();//主键,可自定义

//将相应的视频数据存到数据库中

        VideoBack videoBack = new VideoBack();

        videoBack.setUuid(output_stream_id);

        videoBack.setCaseId(caseId);

        videoBack.setRoomId(roomId);

        videoBack.setPersons(idCards);

        videoBack.setCreateTime(DateTools.getServerDateTime(10));

        this.videoBackMapper.insertSelective(videoBack);

        

        JSONObject  jasonObject = JSONObject.fromObject(map1);//map就是之前页面加载的数据

        Map map = (Map)jasonObject;//将他转成map,其实不转也没事,传json类型也行,但是方法也写好了,懒得改了

        if (playerUrls.length == 1) {//两路画面(一个推流,一个拉流)

            s = TencentLiveUtil.OtherSdhcPushMixStream(pushUrl, playerUrls[0], output_stream_id,map);

        } else if (playerUrls.length == 2) {//三路画面(一个推流,两个拉流)

            System.out.println(playerUrls[0]+"......................"+playerUrls[1]);

            s = TencentLiveUtil.OtherPushMixStream(pushUrl, playerUrls[0], playerUrls[1], output_stream_id,map);

        } else if (playerUrls.length == 3) {//四路画面(一个推流,三个拉流)

System.out.println(playerUrls[0]+"......................"+playerUrls[1]+"......................"+playerUrls[2]);

            s = TencentLiveUtil.SlPushMixStream(pushUrl, playerUrls[0], playerUrls[1], playerUrls[2], output_stream_id,map);

        }

        System.out.println("result=============" + s);

        jsonObject.put("data", s);

        jsonObject.put("output_stream_id", output_stream_id);

        return jsonObject;

    }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

我就主要说说两路画面(其实三路画面,四路画面都一样,只不过多了几个拉流而已)的OtherSdhcPushMixStream的方法,其实他就是将两路画面并称一路画面,叫做混流。


这段代码就是生成混流并录制,该方法写在


/**

* 实地核查混流

* @param

* @param hostStreamId 大主播的流ID

* @param clientSteamId 小主播的流ID

* @param output_stream_id  之前插入数据库的每条数据的主键

* @return

*/

public static String OtherSdhcPushMixStream(String hostStreamId,String clientSteamId,String output_stream_id,Map<String,String>map){

output_stream_id = "mix_"+output_stream_id;  //传给腾讯云,腾讯云会返回这个值,我们根据这个值更新视频地址

String t = Long.toString(new Date().getTime()/1000 + 600);//将获取的时间戳毫秒转换为秒再加60s偏移

String sign = md5Password(LIVE_AUTHENTICATION_KEY + t);  //这段是签名算法,不懂看腾讯云api,我就不写了

//接口地址

String url = "http://fcgi.video.qcloud.com/common_access?appid="+LIVE_APPID+"&interface=Mix_StreamV2&t="+t+"&sign="+sign;

//获取json

String json = otherJsonParam(LIVE_APPID, hostStreamId, clientSteamId, output_stream_id,map);

//调用接口

String result = HttpRequestUtil.sendPost(url, json);

System.out.println("混流结果========"+result);

return result;

}



```/**

* 组装实地核查混流json

* @param appId 直播 APPID

* @param hostStreamId 大主播的流ID

* @param clientSteamId 小主播的流ID

* @param output_stream_id 

* @return

*/

public static String otherJsonParam(String appId,String hostStreamId,String clientSteamId,String output_stream_id,Map<String,String>map){

JSONObject jsonObject = new JSONObject();


jsonObject.put("timestamp",Long.toString(new Date().getTime()));//UNIX时间戳

jsonObject.put("eventId",Long.toString(new Date().getTime()));//混流事件ID,取时间戳即可,后台使用


//添加interface属性

JSONObject interface_Json = new JSONObject();

interface_Json.put("interfaceName", "Mix_StreamV2");

//添加interface的para属性

JSONObject para_Json = new JSONObject();

para_Json.put("appid", appId);

para_Json.put("interface", "mix_streamv2.start_mix_stream_advanced");

para_Json.put("mix_stream_session_id", output_stream_id);

para_Json.put("output_stream_id", output_stream_id);

para_Json.put("output_stream_type", 1);

//添加para的input_stream_list属性

List<JSONObject> input_Stream_Json = new ArrayList<JSONObject>();

JSONObject stream1_Json = new JSONObject();

stream1_Json.put("input_stream_id", "canvas1");


JSONObject stream2_Json = new JSONObject();

stream2_Json.put("input_stream_id", clientSteamId);//拉流


JSONObject stream3_Json = new JSONObject();

stream3_Json.put("input_stream_id", "watermark1");//拉流的人对应的水印


JSONObject stream4_Json = new JSONObject();

stream4_Json.put("input_stream_id", hostStreamId);//推流



JSONObject stream5_Json = new JSONObject();

stream5_Json.put("input_stream_id", "watermark2");//推流流的人对应的水印



//添加大主播的layout_params属相

JSONObject layout_params1 = new JSONObject();

layout_params1.put("image_layer", 1);

layout_params1.put("input_type", 3);

layout_params1.put("color", "0x000000");

layout_params1.put("image_width", 960);

layout_params1.put("image_height", 720);

stream1_Json.put("layout_params", layout_params1);

input_Stream_Json.add(stream1_Json);


JSONObject layout_params2 = new JSONObject();

layout_params2.put("image_layer", 2);

layout_params2.put("image_width", 430);

layout_params2.put("image_height", 700);

layout_params2.put("location_x", 0);

layout_params2.put("location_y", 0);

stream2_Json.put("layout_params", layout_params2);

input_Stream_Json.add(stream2_Json);



JSONObject layout_params3 = new JSONObject();

layout_params3.put("image_layer", 4);

layout_params3.put("input_type", 2);

layout_params3.put("image_width", 80);

layout_params3.put("image_height", 80);

layout_params3.put("location_x", 0);

layout_params3.put("location_y", 60);

layout_params3.put("picture_id",map.get(clientSteamId));//因为clientSteamId是身份证号,map就能根据key值,获取对应的水印id了

stream3_Json.put("layout_params", layout_params3);

input_Stream_Json.add(stream3_Json);




JSONObject layout_params4 = new JSONObject();

layout_params4.put("image_layer", 3);

layout_params4.put("image_width", 430);

layout_params4.put("image_height", 700);

layout_params4.put("location_x", 440);

layout_params4.put("location_y", 0);

stream4_Json.put("layout_params", layout_params4);

input_Stream_Json.add(stream4_Json);



JSONObject layout_params5 = new JSONObject();

layout_params5.put("image_layer", 5);

layout_params5.put("input_type", 2);

layout_params5.put("image_width", 80);

layout_params5.put("image_height", 80);

layout_params5.put("location_x", 440);

layout_params5.put("location_y", 60);

layout_params5.put("picture_id",map.get(hostStreamId));//因为hostStreamId是身份证号,map就能根据key值,获取对应的水印id了

stream5_Json.put("layout_params", layout_params5);

input_Stream_Json.add(stream5_Json);


para_Json.put("input_stream_list", input_Stream_Json);

interface_Json.put("para", para_Json);

jsonObject.put("interface", interface_Json);

return jsonObject.toString();

}


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

然后需要写一个腾讯云回调的接口,该接口需要在腾讯云控制台配置


package cn.meefo.web.front.controller;


import cn.meefo.common.util.GUID2;

import cn.meefo.common.util.TencentLiveUtil;

import cn.meefo.persistence.beans.Person;

import cn.meefo.persistence.beans.PersonExample;

import cn.meefo.persistence.beans.VideFormWithBLOBs;

import cn.meefo.persistence.beans.VideoBack;

import cn.meefo.persistence.mapper.PersonMapper;

import cn.meefo.persistence.mapper.VideFormMapper;

import cn.meefo.persistence.mapper.VideoBackMapper;

import net.sf.json.JSONObject;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;


import java.io.*;

import java.net.HttpURLConnection;

import java.net.MalformedURLException;

import java.net.URL;

import java.util.List;


@Controller("front.tencentCallbackController")

@RequestMapping("/front/tencentback")

public class TencentBackController {

    @Autowired

    private VideoBackMapper videoBackMapper;


    @Autowired

    private PersonMapper personMapper;



    /**

     * 获取腾讯云的消息

     *

     * @param params

     * @return

     */

    @RequestMapping(value = "")

    @ResponseBody

    public JSONObject index(@RequestBody String params) {

        System.out.println("获取的返回值:=================" + params);

        //解析返回数据

        JSONObject jsonObject = JSONObject.fromObject(params);

        if (jsonObject.has("ocrMsg")) {//鉴黄消息

            String type = jsonObject.getString("type");//图片类型

            if ("[1]".equals(type)) {//色情图片


            }

        } else if (jsonObject.has("event_type")) {//其他动作

            String event_type = jsonObject.getString("event_type");

            if ("100".equals(event_type)) {//录像储存完毕

                String video_id = jsonObject.getString("video_id");

                String video_url = jsonObject.getString("video_url");

                String stream_id = jsonObject.getString("stream_id");//直播码

                String file_format = jsonObject.getString("file_format");//文件格式

                String file_id = jsonObject.getString("file_id");//删除操作使用id


                //下载视频到本地

                String path = new GUID2().toString() + "." + file_format;

                try {

                    if (!"mp4".equals(file_format)) {

                        int i = TencentLiveUtil.deleteVodFile(file_id);

                    } else {

                        if ("mix".equals(stream_id.substring(0, 3))) {

                            String liveFormId = stream_id.replaceAll("mix_", "");//获取主键

                            VideoBack videoBack = videoBackMapper.selectByPrimaryKey(liveFormId);

                            videoBack.setVideoUrl(video_url);//更新视频地址

                            videoBackMapper.updateByPrimaryKeySelective(videoBack);//更新视频

                          }

                        } 

                    }



                } catch (Exception e) {

                    // TODO Auto-generated catch block

                    e.printStackTrace();

                }


            } else if ("0".equals(event_type)) {//断流

            }


        }

        JSONObject returnJson = new JSONObject();

        returnJson.put("code", 0);

        return returnJson;

    }

}


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

到了现在终于获取了录制视频的路径。才怪了。

其实水印id就是个坑,你就算将水印id获取,你也不能立刻调用这个id作为参数,否则他会报水印id无法解析成picture_url的错误。我测试了下大约会有15-20秒的延迟,20秒后才能解析这个水印id,因为腾讯后台只是先给你的水印id爽爽,然而还没生成图片。

那我总不能让用户等个20秒再点击录制吧。然后我就提交了工单。




呵呵,picture_url在腾讯文档上根本没这个参数的。

不过前面的生成水印id的方法白写了,直接传图片地址就行,然后我就将map的value值从水印id改为了该项目存储的地址,然后将otherJsonParam里面中jsonparam的组装参数picture_id改为了picture_url。

这样就不会有延迟的问题了

录制下来效果图如下



如果你觉得我的水印很low(我自己也这样觉得)就自己写套更好的水印

————————————————

版权声明:本文为CSDN博主「tzq233」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/qq_39552993/article/details/90737382


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐