文章目录
- 系统全局模块的开发
- 1.系统全文搜索
- 1.1docker 下安装ES以及kibana
- 1.2 配置Es的相关的yaml和configuration
- 1.3 ES全文检索需求
- 视频投稿搜索查询
- 2.观看记录的统计
- 2.1观看视频的添加信息
- 2.2查询观看记录
- 3.用户视频推荐
- 4.视频弹幕遮罩
- 其他章节
系统全局模块的开发
本章主要实现对内容的搜索,根据观看内容推荐相似视频的需求以及弹幕遮照用到的人像分割技术等需求的实现
1.系统全文搜索
1.1docker 下安装ES以及kibana
- 安装参考我的这篇博客:linux下安装docker ;docker下安装ES以及kibana
1.2 配置Es的相关的yaml和configuration
- yml 配置相关连接es的地址
elasticsearch:rest:uris: 192.168.117.130:9200
- Configuration关于Es
@Configuration
public class ElasticSearchConfig extends AbstractElasticsearchConfiguration {@Value("${spring.elasticsearch.rest.uris}")private String esUrl;@Beanpublic RestHighLevelClient elasticsearchClient() {final ClientConfiguration clientConfiguration = ClientConfiguration.builder().connectedTo(esUrl).build();return RestClients.create(clientConfiguration).rest();}
}
也就是要连接对应docker容器上的es,然后RestHighLevelClient是ES提供的一个REST风格的API接口,实际上可以在启动Application启动类里面实现就行
- 第二种实现方案
- 但是需要引入依赖
<!--引入对应的API包--><dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId></dependency>
@Beanpublic RestHighLevelClient client(){return new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.117.130:9200")));}
1.3 ES全文检索需求
- demoApi
@GetMapping("/es-videos")public JsonResponse<Video>getVideos(@RequestParam String keyword){Video video = elasticSearchService.getVideo(keyword);return new JsonResponse<>(video);}
- service层(根据标题模糊查询)
public Video getVideo(String keyword){return videoRepository.findByTitleLike(keyword);}
- dao层
public interface VideoRepository extends ElasticsearchRepository<Video,Long>{//根据关键词进行查询Video findByTitleLike(String keyword);
}
- domain层
- @Field代表指定查询的字段;@Document代表文档
@Data
@Document(indexName = "videos")
public class Video {@Idprivate Long id;@Field(type = FieldType.Long)private Long userId;//用户Idprivate String url;//视频链接private String thumbnail;//封面@Field(type = FieldType.Text)private String title;//标题private String type;//类型0 自制 1 转载private String duration;//时长private String area;//分区private List<VideoTag>videoTagList;//标签列表@Field(type = FieldType.Text)private String description;//简介@Field(type = FieldType.Date)private Date createTime;@Field(type = FieldType.Date)private Date updateTime;
- systemAPI
根据关键词和页码来进行全文检索
@RestController
public class SystemApi {@Autowiredprivate ElasticSearchService elasticSearchService;/*** 全文搜索* @param keyword* @param pageNo* @param pageSize* @return* @throws IOException*/@GetMapping("/contents")public JsonResponse<List<Map<String,Object>>>getContents(@RequestParam String keyword,@RequestParam Integer pageNo,@RequestParam Integer pageSize) throws IOException {List<Map<String,Object>>list = elasticSearchService.getContents(keyword,pageNo,pageSize);return new JsonResponse<>(list);}
}
- ElasticSearchService
@Service
public class ElasticSearchService {@Autowiredprivate VideoRepository videoRepository;@Autowiredprivate UserInfoRepository userInfoRepository;@Autowiredprivate RestHighLevelClient client;//最基本的存储public void addVideo(Video video){videoRepository.save(video);}public Video getVideo(String keyword){return videoRepository.findByTitleLike(keyword);}public void deleteAllVideos(){videoRepository.deleteAll();}public void addUserInfo(UserInfo userInfo){userInfoRepository.save(userInfo);}//分页请求public List<Map<String,Object>>getContents(String keyword,Integer pageNo,Integer pageSize) throws IOException {String[]indices = new String[]{"video","user-infos"};//索引库SearchRequest searchRequest = new SearchRequest(indices);//构建request//构建分页SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();sourceBuilder.from(pageNo-1);sourceBuilder.size(pageSize);MultiMatchQueryBuilder matchQueryBuilder = QueryBuilders.multiMatchQuery(keyword,"title","nick","description");sourceBuilder.query(matchQueryBuilder);searchRequest.source(sourceBuilder);//高亮显示HighlightBuilder highlightBuilder = new HighlightBuilder();String[]array = {"title","nick","description"};for(String key:array){highlightBuilder.fields().add(new HighlightBuilder.Field(key));}highlightBuilder.requireFieldMatch(false);highlightBuilder.preTags("<span style=\"color:red\">");highlightBuilder.postTags("</span>");sourceBuilder.highlighter(highlightBuilder);//解析结果SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);List<Map<String,Object>>arrayList = new ArrayList<>();//hit->是指查询出来的内容for(SearchHit hit:searchResponse.getHits()){//处理高亮字段Map<String, HighlightField>highLightBuilderFields = hit.getHighlightFields();//获取结果中高亮的部分Map<String,Object>sourceMap = hit.getSourceAsMap();for(String key:array){HighlightField field = highLightBuilderFields.get(key);if(field!=null){Text[]fragments = field.getFragments();String str = Arrays.toString(fragments);str = str.substring(1,str.length()-1);sourceMap.put(key,str);}}arrayList.add(sourceMap);}return arrayList;}
}
- es用法笔记博客ES使用笔记
视频投稿搜索查询
在第四章的基础上,我们加上ES搜索的save方法即可
- api层
/*** 视频投稿* @param video* @return*/@PostMapping("/videos")public JsonResponse<String>addVideos(@RequestBody Video video){Long userId = userSupport.getCurrentUserId();video.setUserId(userId);videoService.addVideos(video);elasticSearchService.addVideo(video);return JsonResponse.success();}
- ElasticService层;利用ES基本功能进行包装最基础的查询,删除,分页,高亮等…
public void addVideo(Video video){videoRepository.save(video);}
-
进行测试
-
新增视频测试
-
查询结果
-
进行分页查询
2.观看记录的统计
- 观看记录无论是游客还是用户都是要记录的,但是每个用户和游客一天只能算一个播放量,游客可以用ip+电脑操作系统来记录
- 实现思路:创建一个用户观看表,包含userId,videoId,ip,clientId,前端传给我们一个videoView和httpRequest,包含了这些信息,然后service层里面进行是游客还是用户,是第一次还是多次,只有第一次才进行添加,查询该观看记录视频的参数还是可以参考之前的用一个map来封装,在根据查到的进行判断是否为空,为空代表第一次,将用户的信息set进去再齿形添加,对于查询观看数量,我们直接count查询就行
- 依赖
<!--引入获取Ip和电脑信息--><dependency><groupId>eu.bitwalker</groupId><artifactId>UserAgentUtils</artifactId><version>1.21</version></dependency>
- 观看记录表
DROP TABLE IF EXISTS `t_video_view`;
CREATE TABLE `t_video_view` (`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键id',`videoId` BIGINT NOT NULL COMMENT '视频id',`userId` BIGINT DEFAULT NULL COMMENT '用户id',`clientId` VARCHAR(500) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '客户端id',`ip` VARCHAR(50) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'ip',`createTime` DATETIME DEFAULT NULL COMMENT '创建时间',PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3 COMMENT='视频观看记录表';
2.1观看视频的添加信息
- Controller
@PostMapping("/video-views")public JsonResponse<String>addVideoView(@RequestBody VideoView videoView,HttpServletRequest request){Long userId;try{userId = userSupport.getCurrentUserId();videoView.setUserId(userId);videoService.addVideoView(videoView,request);}catch (Exception e){videoService.addVideoView(videoView,request);}return JsonResponse.success();}
- service
- 由于我们要获取操作系统,因此我们要知道传request是为了获得请求头中的
user-agent
public void addVideoView(VideoView videoView, HttpServletRequest request) {Long videoId = videoView.getVideoId();Long userId = videoView.getUserId();//生成clientIdString agent = request.getHeader("User-Agent");UserAgent userAgent = UserAgent.parseUserAgentString(agent);String clientId = String.valueOf(userAgent.getId());String ip = IpUtil.getIP(request);Map<String,Object>params = new HashMap<>();if(userId != null){//不是访客params.put("userId",userId);}else{params.put("ip",ip);params.put("clientId",clientId);}Date now = new Date();SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");params.put("today",sdf.format(now));params.put("videoId",videoId);//查看是否存在VideoView dbVideoView = videoDao.getVideoView(params);//每天只需要记录一次观看记录if(dbVideoView== null){videoView.setIp(ip);videoView.setClientId(clientId);videoView.setCreateTime(new Date());videoDao.addVideoView(videoView);}}
- IPutil
package com.imooc.bilibili.service.util;import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;/*** IP工具类*/
public class IpUtil {public static String getIP(HttpServletRequest httpServletRequest) {String ip = httpServletRequest.getHeader("X-Forwarded-For");if (ip != null && !"".equals(ip.trim())) {int index = ip.indexOf(",");if (index != -1) {ip = ip.substring(0, index);}return ip;} else {ip = httpServletRequest.getHeader("X-Real-IP");if (ip == null || "".equals(ip.trim())) {ip = httpServletRequest.getRemoteAddr();}return ip;}}public static String getLocalAddress() {try {InetAddress candidateAddress = null;Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces();while (ifaces.hasMoreElements()) {NetworkInterface iface = ifaces.nextElement();Enumeration<InetAddress> inetAddrs = iface.getInetAddresses();while (inetAddrs.hasMoreElements()) {InetAddress inetAddr = inetAddrs.nextElement();if (!inetAddr.isLoopbackAddress()) {if (inetAddr.isSiteLocalAddress()) {return inetAddr.getHostAddress();}if (candidateAddress == null) {candidateAddress = inetAddr;}}}}if (candidateAddress != null) {return candidateAddress.getHostAddress();} else {InetAddress jdkSuppliedAddress = InetAddress.getLocalHost();return jdkSuppliedAddress.getHostAddress();}} catch (Exception var5) {return "unkown";}}
}
- getVideoView & addVideoView
<-- getVideoView是需要进行复杂查询的,对于clientId我们查询的是clientId 不为NULL 但是 userId为NULL,这样就代表将访客给查询到了!--><!--查询对应的观看记录--><select id="getVideoView" resultType="com.imooc.bilibili.domain.VideoView" parameterType="java.util.Map">select*fromt_video_viewwherevideoId = #{videoId}<if test="userId==null and clientId != null">and clientId = #{clientId} and userId is null</if><if test="userId != null">and userId = #{userId}</if><if test="ip != null and ip !='' ">and ip = #{ip}</if><if test="today != null and today != '' ">and DATE_FORMATE(createTime,'%Y-%m-%d') = #{today}</if></select><insert id="addVideoView" parameterType="com.imooc.bilibili.domain.VideoView" >insert intot_video_view(videoId,userId,clientId,ip,createTime)values(#{videoId},#{userId},#{clientId},#{ip},#{createTime})</insert>
2.2查询观看记录
/*** 查看视频观看人数记录* @param videoId* @return*/@GetMapping("/video-view-counts")public JsonResponse<Integer>getVideoViewCounts(@RequestParam Long videoId){Integer count = videoService.getVideoViewCount(videoId);return new JsonResponse<>(count);}
- servcie层
public Integer getVideoViewCount(Long videoId) {return videoDao.getVideoViewCount(videoId);}
- getVideoViewCount.xml
<!--查看观看记录人数--><select id="getVideoViewCount" resultType="java.lang.Integer" parameterType="java.lang.Integer">selectcount(1)fromt_video_viewwherevideoId = #{videoId};</select>
3.用户视频推荐
- 这里我们采用第三方库包提供的推荐算法
<!--引入推荐引擎mahout,注意要先全部引入,再使用exclusion标签--><dependency><groupId>org.apache.mahout</groupId><artifactId>mahout-mr</artifactId><version>0.12.2</version><exclusions><exclusion><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId></exclusion><exclusion><groupId>org.slf4j</groupId><artifactId>slf4j-jcl</artifactId></exclusion><exclusion><groupId>org.apache.lucene</groupId><artifactId>lucene-core</artifactId></exclusion><exclusion><groupId>org.apache.lucene</groupId><artifactId>lucene-analyzers-common</artifactId></exclusion><exclusion><groupId>log4j</groupId><artifactId>log4j</artifactId></exclusion><exclusion><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId></exclusion><exclusion><artifactId>jersey-client</artifactId><groupId>com.sun.jersey</groupId></exclusion><exclusion><artifactId>jersey-core</artifactId><groupId>com.sun.jersey</groupId></exclusion><exclusion><artifactId>jersey-apache-client4</artifactId><groupId>com.sun.jersey.contribs</groupId></exclusion></exclusions></dependency>
-
用户视频推荐表
-
推荐算法用sql可以这样进行计算
-
视频内容推荐RestController(推荐视频所以返回一个Listlist的集合)
/*** 根据用户喜好推送视频* @return*/@GetMapping("/recommendations")public JsonResponse<List<Video>>recommend() throws TasteException {Long userId = userSupport.getCurrentUserId();List<Video>list = videoService.recommend(userId);return new JsonResponse<>(list);}
- service
-
根据用户喜好,建立模型数据,再获取对应的相似度和相邻的视频内容,最后得到一个recoomend系数
//获取用户喜好,进行视频的推送public List<Video> recommend(Long userId) throws TasteException {List<UserPreference>list = videoDao.getAllUserPreference();//创建模型数据DataModel dataModel = this.createDataModel(list);//2.创建similar相似度UserSimilarity similarity = new UncenteredCosineSimilarity(dataModel);//3.获取用户userNeighborhoodSystem.out.println(similarity.userSimilarity(7,8));UserNeighborhood userNeighborhood = new NearestNUserNeighborhood(2,similarity,dataModel);long[]ar = userNeighborhood.getUserNeighborhood(userId);//4.构建推荐器recommendRecommender recommender = new GenericUserBasedRecommender(dataModel,userNeighborhood,similarity);List<RecommendedItem>recommendedItems = recommender.recommend(userId,5);//展示类似的5个视频List<Long>itemIds = recommendedItems.stream().map(RecommendedItem::getItemID).collect(Collectors.toList());return videoDao.batchGetVideoByIds(itemIds);}private DataModel createDataModel(List<UserPreference> userPreferenceList) {FastByIDMap<PreferenceArray> fastByIdMap = new FastByIDMap<>();Map<Long, List<UserPreference>> map = userPreferenceList.stream().collect(Collectors.groupingBy(UserPreference::getUserId));Collection<List<UserPreference>> list = map.values();for(List<UserPreference> userPreferences : list){GenericPreference[] array = new GenericPreference[userPreferences.size()];for(int i = 0; i < userPreferences.size(); i++){UserPreference userPreference = userPreferences.get(i);GenericPreference item = new GenericPreference(userPreference.getUserId(), userPreference.getVideoId(), userPreference.getValue());array[i] = item;}fastByIdMap.put(array[0].getUserID(), new GenericUserPreferenceArray(Arrays.asList(array)));}return new GenericDataModel(fastByIdMap);}
- Userprefrence
@Data
public class UserPreference {private Long id;private Long userId;private Long videoId;private Float value;private Date createTime;
}
- dao
List<UserPreference> getAllUserPreference();List<Video> batchGetVideoByIds(@Param("idList") List<Long> idList);
- batchGetVideo
<!--通过id批计算喜好度视频,这个也可以自定义计算参数--><select id="getAllUserPreference" resultType="com.imooc.bilibili.domain.UserPreference">selectuserId,videoId,sum(case operationTypewhen '0' then 6when '1' then 2when '2' then 2else 0 end)as `value`fromt_video_operationgroup by userId,videoId;</select><!--通过id批量查询视频--><select id="batchGetVideoByIds" resultType="com.imooc.bilibili.domain.Video">select*fromt_videowhereid in<foreach collection="idList" item="id" open="(" close=")" separator=",">#{id}</foreach></select>
- 测试
4.视频弹幕遮罩
这一块是基于百度API中的人像分隔我还有点蒙,回头再看看,具体如下
- yml:需要获取对应百度API的key等信息
具体百度AI文档:https://ai.baidu.com/ai-doc/REFERENCE/Ck3dwjgn3
- 相关依赖
<dependency><groupId>org.bytedeco</groupId><artifactId>javacv</artifactId><version>1.4.3</version></dependency><dependency><groupId>org.bytedeco.javacpp-presets</groupId><artifactId>ffmpeg-platform</artifactId><version>4.0.2-1.4.3</version></dependency>
- controller
/*** 视频帧截取生成黑白剪影*/@GetMapping("/video-frames")public JsonResponse<List<VideoBinaryPicture>> captureVideoFrame(@RequestParam Long videoId,@RequestParam String fileMd5) throws Exception {List<VideoBinaryPicture> list = videoService.convertVideoToImage(videoId, fileMd5);return new JsonResponse<>(list);}/*** 查询视频黑白剪影*/@GetMapping("/video-binary-images")public JsonResponse<List<VideoBinaryPicture>> getVideoBinaryImages(@RequestParam Long videoId,Long videoTimestamp,String frameNo) {Map<String, Object> params = new HashMap<>();params.put("videoId", videoId);params.put("videoTimestamp", videoTimestamp);params.put("frameNo", frameNo);List<VideoBinaryPicture> list = videoService.getVideoBinaryImages(params);return new JsonResponse<>(list);}
- service
public List<VideoBinaryPicture> convertVideoToImage(Long videoId, String fileMd5) throws Exception {File file = fileService.getFileByMd5(fileMd5);String filePath = "E:\\WorkSpace\\tmp"+videoId + "."+ file.getType();fastDFSUtil.downLoadFile(file.getUrl(),filePath);FFmpegFrameGrabber fFmpegFrameGrabber = FFmpegFrameGrabber.createDefault(filePath);fFmpegFrameGrabber.start();//启动int ffLength = fFmpegFrameGrabber.getLengthInFrames();//获取帧Frame frame;Java2DFrameConverter converter = new Java2DFrameConverter();int count = 1;List<VideoBinaryPicture>pictures = new ArrayList<>();for(int i =1;i<=ffLength;i++){Long timestamp = fFmpegFrameGrabber.getTimestamp();frame = fFmpegFrameGrabber.grabImage();if(count == i){if(frame == null){throw new ConditionalException("无效帧");}BufferedImage bufferedImage = converter.getBufferedImage(frame);ByteArrayOutputStream os = new ByteArrayOutputStream();ImageIO.write(bufferedImage,"png",os);InputStream inputStream = new ByteArrayInputStream(os.toByteArray());//输出黑白剪影文件java.io.File outputFile = java.io.File.createTempFile("convert-"+videoId+"-",".png");BufferedImage binaryImg = imageUtil.getBodyOutline(bufferedImage, inputStream);ImageIO.write(binaryImg,"png",outputFile);//有的浏览器或网站需要把图片白色的部分转为透明色,使用以下方法可实现imageUtil.transferAlpha(outputFile,outputFile);//上传视频剪影文件String imgUrl = fastDFSUtil.uploadCommonFile(outputFile,"png");VideoBinaryPicture videoBinaryPicture = new VideoBinaryPicture();videoBinaryPicture.setFrameNo(i);videoBinaryPicture.setUrl(imgUrl);videoBinaryPicture.setVideoId(videoId);videoBinaryPicture.setVideoTimeStamp(timestamp);pictures.add(videoBinaryPicture);count += FRAME_NO;//删除临时文件outputFile.delete();}}//删除临时文件java.io.File tmpFile = new java.io.File(filePath);tmpFile.delete();//批量添加视频剪影文件videoDao.batchAddVideoBinaryPictures(pictures);return pictures;}public List<VideoBinaryPicture> getVideoBinaryImages(Map<String, Object> params) {return videoDao.getVideoBinaryImages(params);}
- dao
Integer batchAddVideoBinaryPictures(@Param("pictureList") List<VideoBinaryPicture> pictureList);List<VideoBinaryPicture> getVideoBinaryImages(Map<String, Object> params);
- xml
<!--添加视频分隔文件--><insert id="batchAddVideoBinaryPictures" parameterType="java.util.List">insert intot_video_binary_picture(videoId,frameNo,url,videoTimestamp,createTime)values<foreach collection="pictureList" item="picture" separator=",">(#{picture.videoId},#{picture.frameNo},#{picture.url},#{picture.videoTimestamp},#{picture.createTime})</foreach></insert><!--查看视频二值化--><select id="getVideoBinaryImages" resultType="com.imooc.bilibili.domain.VideoBinaryPicture" parameterType="java.util.Map">select*fromt_video_binary_picturewhere<if test="frameNo != null and frameNo != '' ">and frameNo = #{frameNo}</if><if test="videoTimestamp != null">and videoTimestamp = #{videoTimestamp}</if></select>
其他章节
第二章
第三章
第四章