video.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. package controllers
  2. import (
  3. "encoding/binary"
  4. "encoding/json"
  5. "eta/eta_mini_crm_ht/models"
  6. "eta/eta_mini_crm_ht/models/request"
  7. "eta/eta_mini_crm_ht/models/response"
  8. "eta/eta_mini_crm_ht/services"
  9. "eta/eta_mini_crm_ht/services/elastic"
  10. "eta/eta_mini_crm_ht/utils"
  11. "fmt"
  12. "github.com/rdlucklib/rdluck_tools/paging"
  13. "io"
  14. "os"
  15. "path"
  16. "strconv"
  17. "time"
  18. )
  19. var (
  20. videoType = map[string]int{
  21. "mp4": 1,
  22. "m4a": 1,
  23. }
  24. )
  25. type VideoController struct {
  26. BaseAuthController
  27. }
  28. // UploadVideo @Title 上传视频
  29. // @Description 上传视频
  30. // @Param File query file true "文件"
  31. // @Success 200 {object} models.ReportAuthorResp
  32. // @router /uploadVideo [post]
  33. func (this *VideoController) UploadVideo() {
  34. br := new(models.BaseResponse).Init()
  35. defer func() {
  36. this.Data["json"] = br
  37. this.ServeJSON()
  38. }()
  39. f, h, err := this.GetFile("File")
  40. if err != nil {
  41. br.Msg = "获取资源信息失败"
  42. br.ErrMsg = "获取资源信息失败,Err:" + err.Error()
  43. return
  44. }
  45. defer f.Close()
  46. size, err := strconv.Atoi(utils.UPLOAD_VIDEO_SIZE)
  47. if err != nil {
  48. size = 1024
  49. }
  50. if h.Size > 1024*1024*int64(size) {
  51. br.Msg = fmt.Sprintf("视频大小不能超过%dM", size)
  52. br.ErrMsg = "视频上传失败,Err:" + err.Error()
  53. return
  54. }
  55. ext := path.Ext(h.Filename)
  56. if _, ok := videoType[ext]; !ok {
  57. br.Msg = "视频格式不正确"
  58. br.ErrMsg = "视频上传失败,Err:" + err.Error()
  59. return
  60. }
  61. dateDir := time.Now().Format("20060102")
  62. uploadDir := utils.STATIC_DIR + "ht/audio" + dateDir
  63. err = os.MkdirAll(uploadDir, utils.DIR_MOD)
  64. if err != nil {
  65. br.Msg = "存储目录创建失败"
  66. br.ErrMsg = "存储目录创建失败,Err:" + err.Error()
  67. return
  68. }
  69. randStr := utils.GetRandStringNoSpecialChar(28)
  70. fileName := randStr + ext
  71. fpath := uploadDir + "/" + fileName
  72. err = this.SaveToFile("File", fpath)
  73. if err != nil {
  74. br.Msg = "视频上传失败"
  75. br.ErrMsg = "视频上传失败,Err:" + err.Error()
  76. return
  77. }
  78. duration, err := GetMP4Duration(f)
  79. if err != nil {
  80. br.Msg = "视频上传失败"
  81. br.ErrMsg = "获取视频时长失败,Err:" + err.Error()
  82. return
  83. }
  84. audioUploadDir := utils.RESOURCE_DIR + "video/"
  85. savePdfToOssPath := audioUploadDir + time.Now().Format("200601/20060102/")
  86. audioName := utils.GetRandStringNoSpecialChar(28)
  87. savePdfToOssPath += audioName + ext
  88. defer func() {
  89. err = os.Remove(fpath)
  90. fmt.Sprintf("删除文件失败:%v", err)
  91. }()
  92. ossClient := services.NewOssClient()
  93. if ossClient == nil {
  94. br.Msg = "视频上传失败"
  95. br.ErrMsg = "初始化OSS服务失败"
  96. return
  97. }
  98. videoUrl, err := ossClient.UploadFile("", fpath, savePdfToOssPath)
  99. if err != nil {
  100. br.Msg = "视频上传失败"
  101. br.ErrMsg = "视频上传失败,Err:" + err.Error()
  102. return
  103. }
  104. base := path.Base(h.Filename)
  105. resp := new(response.MediaUploadResp)
  106. resp.Url = videoUrl
  107. resp.FileName = base
  108. resp.DurationMillisecond = duration * 1000
  109. br.Data = resp
  110. br.Msg = "上传成功"
  111. br.Ret = 200
  112. br.Success = true
  113. }
  114. // AddVideo @Title 新增音频
  115. // @Description 新增音频
  116. // @Param File query file true "文件"
  117. // @Success 200 {object} models.ReportAuthorResp
  118. // @router /addVideo [post]
  119. func (this *VideoController) AddVideo() {
  120. br := new(models.BaseResponse).Init()
  121. defer func() {
  122. this.Data["json"] = br
  123. this.ServeJSON()
  124. }()
  125. var req request.VideoReq
  126. if err := json.Unmarshal(this.Ctx.Input.RequestBody, &req); err != nil {
  127. br.Msg = "参数解析失败"
  128. br.ErrMsg = "参数解析失败,Err:" + err.Error()
  129. return
  130. }
  131. if req.VideoName == "" {
  132. br.Msg = "视频名称为空"
  133. return
  134. }
  135. if req.AnalystId == 0 {
  136. br.Msg = "研究员ID为空"
  137. return
  138. }
  139. if req.AnalystName == "" {
  140. br.Msg = "研究员名称为空"
  141. return
  142. }
  143. if req.SrcUrl == "" {
  144. br.Msg = "视频地址为空"
  145. return
  146. }
  147. if req.DurationMillisecond == 0 {
  148. br.Msg = "视频时长为空"
  149. return
  150. }
  151. if req.CoverSrc == "" {
  152. br.Msg = "封面为空"
  153. return
  154. }
  155. var err error
  156. audioInsert := &models.Media{
  157. AuthorId: req.AnalystId,
  158. AuthorName: req.AnalystName,
  159. MediaType: models.Video,
  160. Src: req.SrcUrl,
  161. CoverSrc: req.CoverSrc,
  162. MediaName: req.VideoName,
  163. SourceType: "mp4",
  164. MediaPlayMilliseconds: req.DurationMillisecond,
  165. PermissionIds: req.PermissionIds,
  166. PublishedTime: time.Now(),
  167. SendStatus: models.UNSEND,
  168. Deleted: 0,
  169. CreatedTime: time.Now(),
  170. }
  171. err = models.InsertMedia(audioInsert)
  172. if err != nil {
  173. br.Msg = "添加失败"
  174. br.ErrMsg = "音频新增失败,Err:" + err.Error()
  175. return
  176. }
  177. // 添加es
  178. go func(audio *models.ESMedia) {
  179. docId := strconv.Itoa(audio.MediaId)
  180. err = elastic.EsAddOrEditMedia(utils.MEDIA_INDEX, docId, audio)
  181. if err != nil {
  182. utils.FileLog.Info("音频记录es新增失败,Err:" + err.Error())
  183. return
  184. }
  185. utils.FileLog.Info("音频记录es新增成功, pdfId:" + docId)
  186. }(audioInsert.ToView())
  187. // 创建消息
  188. _, _ = services.CreateMeta(audioInsert.AuthorName, audioInsert.AuthorId, audioInsert.Id, audioInsert.PublishedTime.Format(time.DateTime), models.VideoSourceType)
  189. br.Msg = "添加成功"
  190. br.Ret = 200
  191. br.Success = true
  192. }
  193. // EditVideo @Title 编辑音频
  194. //
  195. // @Description 编辑音频
  196. // @Success 200 {object} models.ReportAuthorResp
  197. // @router /editVideo [post]
  198. func (this *VideoController) EditVideo() {
  199. br := new(models.BaseResponse).Init()
  200. defer func() {
  201. this.Data["json"] = br
  202. this.ServeJSON()
  203. }()
  204. var req request.VideoReq
  205. if err := json.Unmarshal(this.Ctx.Input.RequestBody, &req); err != nil {
  206. br.Msg = "参数解析失败"
  207. br.ErrMsg = "参数解析失败,Err:" + err.Error()
  208. return
  209. }
  210. if req.VideoId <= 0 {
  211. br.Msg = "视频ID为空"
  212. return
  213. }
  214. if req.VideoName == "" {
  215. br.Msg = "视频名称为空"
  216. return
  217. }
  218. if req.AnalystId == 0 {
  219. br.Msg = "研究员ID为空"
  220. return
  221. }
  222. if req.AnalystName == "" {
  223. br.Msg = "研究员名称为空"
  224. return
  225. }
  226. if req.SrcUrl == "" {
  227. br.Msg = "视频地址为空"
  228. return
  229. }
  230. if req.DurationMillisecond == 0 {
  231. br.Msg = "视频时长为空"
  232. return
  233. }
  234. if req.CoverSrc == "" {
  235. br.Msg = "封面为空"
  236. return
  237. }
  238. var err error
  239. audioEdit := &models.Media{
  240. Id: req.VideoId,
  241. AuthorId: req.AnalystId,
  242. AuthorName: req.AnalystName,
  243. MediaType: models.Video,
  244. CoverSrc: req.CoverSrc,
  245. Src: req.SrcUrl,
  246. SourceType: "mp4",
  247. MediaName: req.VideoName,
  248. MediaPlayMilliseconds: req.DurationMillisecond,
  249. PermissionIds: req.PermissionIds,
  250. }
  251. //coverSrc
  252. _, err = models.UpdateMedia(audioEdit)
  253. if err != nil {
  254. br.Msg = "更新失败"
  255. br.ErrMsg = "视频更新失败,Err:" + err.Error()
  256. return
  257. }
  258. go func(audio *models.ESMedia) {
  259. docId := strconv.Itoa(audio.MediaId)
  260. err = elastic.EsAddOrEditMedia(utils.MEDIA_INDEX, docId, audio)
  261. if err != nil {
  262. utils.FileLog.Info("视频记录es更新失败,Err:" + err.Error())
  263. return
  264. }
  265. utils.FileLog.Info("视频记录es更新成功, pdfId:" + docId)
  266. }(audioEdit.ToView())
  267. br.Msg = "编辑成功"
  268. br.Ret = 200
  269. br.Success = true
  270. }
  271. // DeleteVideo
  272. // @Title 删除音频
  273. //
  274. // @Description 删除音频
  275. // @Success 200 {object} models.ReportAuthorResp
  276. // @router /deleteVideo [post]
  277. func (this *VideoController) DeleteVideo() {
  278. br := new(models.BaseResponse).Init()
  279. defer func() {
  280. this.Data["json"] = br
  281. this.ServeJSON()
  282. }()
  283. var req request.VideoReq
  284. if err := json.Unmarshal(this.Ctx.Input.RequestBody, &req); err != nil {
  285. br.Msg = "参数解析失败"
  286. br.ErrMsg = "参数解析失败,Err:" + err.Error()
  287. return
  288. }
  289. if req.VideoId <= 0 {
  290. br.Msg = "视频ID为空"
  291. return
  292. }
  293. var err error
  294. audioDelete := &models.Media{
  295. Id: req.VideoId,
  296. Deleted: 1,
  297. }
  298. err = models.DeleteMedia(audioDelete)
  299. if err != nil {
  300. br.Msg = "删除失败"
  301. br.ErrMsg = "视频删除失败,Err:" + err.Error()
  302. return
  303. }
  304. // 添加es
  305. go func(audio *models.ESMedia) {
  306. docId := strconv.Itoa(audio.MediaId)
  307. err = elastic.EsDeleteData(utils.MEDIA_INDEX, docId)
  308. if err != nil {
  309. utils.FileLog.Info("视频记录es删除失败,Err:" + err.Error())
  310. return
  311. }
  312. utils.FileLog.Info("视频记录es删除成功, pdfId:" + docId)
  313. }(audioDelete.ToView())
  314. br.Msg = "删除成功"
  315. br.Ret = 200
  316. br.Success = true
  317. }
  318. // VideoList
  319. // @Title 研报列表
  320. // @Description pdf研报列表
  321. // @Param PageSize query int true "每页数据条数"
  322. // @Param CurrentIndex query int true "当前页页码,从1开始"
  323. // @Param ClassifyIds query string true "二级分类id,可多选用英文,隔开"
  324. // @Param KeyWord query string true "报告标题/创建人"
  325. // @Param SortType query string true "排序方式"
  326. // @Success 200 {object} models.ReportAuthorResp
  327. // @router /videoList [get]
  328. func (this *VideoController) VideoList() {
  329. br := new(models.BaseResponse).Init()
  330. defer func() {
  331. this.Data["json"] = br
  332. this.ServeJSON()
  333. }()
  334. pageSize, _ := this.GetInt("PageSize")
  335. currentIndex, _ := this.GetInt("CurrentIndex")
  336. sortType := this.GetString("SortType")
  337. KeyWord := this.GetString("KeyWord")
  338. var condition string
  339. var pars []interface{}
  340. if pageSize <= 0 {
  341. pageSize = utils.PageSize20
  342. }
  343. if currentIndex <= 0 {
  344. currentIndex = 1
  345. }
  346. if KeyWord != "" {
  347. condition += " AND media_name like '%" + KeyWord + "%'"
  348. }
  349. sortCondition := " ORDER BY published_time "
  350. if sortType == "" {
  351. sortType = "DESC"
  352. }
  353. sortCondition = sortCondition + sortType
  354. total, err := models.GetMediaCountByCondition(models.Video, condition, pars)
  355. if err != nil {
  356. br.Msg = "获取视频列表失败"
  357. br.ErrMsg = "获取视频列表统计失败,Err:" + err.Error()
  358. return
  359. }
  360. startSize := utils.StartIndex(currentIndex, pageSize)
  361. List, err := models.GetMediaByCondition(models.Video, condition, sortCondition, pars, startSize, pageSize)
  362. if err != nil {
  363. br.Msg = "获取视频列表失败"
  364. br.ErrMsg = "获取视频列表失败,Err:" + err.Error()
  365. return
  366. }
  367. var reportViewList []*models.MediaView
  368. for _, report := range List {
  369. reportView := report.ToMediaView()
  370. reportViewList = append(reportViewList, reportView)
  371. }
  372. page := paging.GetPaging(currentIndex, pageSize, total)
  373. resp := new(response.MediaListResp)
  374. resp.List = reportViewList
  375. resp.Paging = page
  376. br.Ret = 200
  377. br.Success = true
  378. br.Data = resp
  379. br.Msg = "获取成功"
  380. }
  381. // UploadFile @Title 上传图片
  382. // @Description 上传视频
  383. // @Param File query file true "文件"
  384. // @Success 200 {object} models.ReportAuthorResp
  385. // @router /uploadFile [post]
  386. func (this *VideoController) UploadFile() {
  387. br := new(models.BaseResponse).Init()
  388. defer func() {
  389. this.Data["json"] = br
  390. this.ServeJSON()
  391. }()
  392. f, h, err := this.GetFile("File")
  393. if err != nil {
  394. br.Msg = "获取资源信息失败"
  395. br.ErrMsg = "获取资源信息失败,Err:" + err.Error()
  396. return
  397. }
  398. defer f.Close()
  399. size, err := strconv.Atoi(utils.UPLOAD_IMG_SIZE)
  400. if err != nil {
  401. size = 100
  402. }
  403. if h.Size > 1024*1024*int64(size) {
  404. br.Msg = fmt.Sprintf("图片大小不能超过%dK", size)
  405. br.ErrMsg = "图片上传失败,Err:" + err.Error()
  406. return
  407. }
  408. ext := path.Ext(h.Filename)
  409. dateDir := time.Now().Format("20060102")
  410. uploadDir := utils.STATIC_DIR + "ht/audio" + dateDir
  411. err = os.MkdirAll(uploadDir, utils.DIR_MOD)
  412. if err != nil {
  413. br.Msg = "存储目录创建失败"
  414. br.ErrMsg = "存储目录创建失败,Err:" + err.Error()
  415. return
  416. }
  417. randStr := utils.GetRandStringNoSpecialChar(28)
  418. fileName := randStr + ext
  419. fpath := uploadDir + "/" + fileName
  420. err = this.SaveToFile("File", fpath)
  421. if err != nil {
  422. br.Msg = "图片上传失败"
  423. br.ErrMsg = "图片上传失败,Err:" + err.Error()
  424. return
  425. }
  426. audioUploadDir := utils.RESOURCE_DIR + "img/"
  427. savePdfToOssPath := audioUploadDir + time.Now().Format("200601/20060102/")
  428. audioName := utils.GetRandStringNoSpecialChar(28)
  429. savePdfToOssPath += audioName + ext
  430. defer func() {
  431. err = os.Remove(fpath)
  432. fmt.Sprintf("删除文件失败:%v", err)
  433. }()
  434. ossClient := services.NewOssClient()
  435. if ossClient == nil {
  436. br.Msg = "图片上传失败"
  437. br.ErrMsg = "初始化OSS服务失败"
  438. return
  439. }
  440. imageUrl, err := ossClient.UploadFile("", fpath, savePdfToOssPath)
  441. if err != nil {
  442. br.Msg = "图片上传失败"
  443. br.ErrMsg = "图片上传失败,Err:" + err.Error()
  444. return
  445. }
  446. base := path.Base(h.Filename)
  447. resp := new(response.MediaUploadResp)
  448. resp.Url = imageUrl
  449. resp.FileName = base
  450. br.Data = resp
  451. br.Msg = "上传成功"
  452. br.Ret = 200
  453. br.Success = true
  454. }
  455. // BoxHeader represents the header of an ISO BMFF box.
  456. type BoxHeader struct {
  457. Size uint32
  458. Type [4]byte
  459. Size64 uint64
  460. }
  461. // getHeaderBoxInfo parses the box header from the provided byte slice.
  462. func getHeaderBoxInfo(data []byte) BoxHeader {
  463. var header BoxHeader
  464. header.Size = binary.BigEndian.Uint32(data[0:4])
  465. copy(header.Type[:], data[4:8])
  466. if header.Size == 1 {
  467. header.Size64 = binary.BigEndian.Uint64(data[8:16])
  468. }
  469. return header
  470. }
  471. // getFourccType returns the FourCC type as a string.
  472. func getFourccType(header BoxHeader) string {
  473. return string(header.Type[:])
  474. }
  475. // readBox reads the entire box data from the reader at the given offset.
  476. func readBox(reader io.ReaderAt, offset int64, size uint64) ([]byte, error) {
  477. boxData := make([]byte, size)
  478. _, err := reader.ReadAt(boxData, offset)
  479. if err != nil {
  480. return nil, err
  481. }
  482. return boxData, nil
  483. }
  484. // findBox recursively finds a box of the specified type within the given box data.
  485. func findBox(boxData []byte, boxType string) ([]byte, error) {
  486. var offset uint32 = 0
  487. for offset < uint32(len(boxData)) {
  488. header := getHeaderBoxInfo(boxData[offset:])
  489. size := uint64(header.Size)
  490. if header.Size == 1 {
  491. size = header.Size64
  492. }
  493. if getFourccType(header) == boxType {
  494. return boxData[offset : offset+uint32(size)], nil
  495. }
  496. offset += uint32(size)
  497. }
  498. return nil, fmt.Errorf("box type %s not found", boxType)
  499. }
  500. // GetMP4Duration reads the duration of an MP4 or M4A file from the provided reader.
  501. func GetMP4Duration(reader io.ReaderAt) (lengthOfTime int, err error) {
  502. var info = make([]byte, 0x10)
  503. var boxHeader BoxHeader
  504. var offset int64 = 0
  505. // 获取moov结构偏移
  506. for {
  507. _, err = reader.ReadAt(info, offset)
  508. if err == io.EOF {
  509. fmt.Println("Reached EOF without finding moov box.")
  510. return 0, fmt.Errorf("moov box not found")
  511. }
  512. if err != nil {
  513. return 0, fmt.Errorf("error reading at offset %d: %w", offset, err)
  514. }
  515. boxHeader = getHeaderBoxInfo(info)
  516. fourccType := getFourccType(boxHeader)
  517. if fourccType == "moov" {
  518. fmt.Println("Found moov box at offset", offset)
  519. break
  520. }
  521. nextOffset := int64(boxHeader.Size)
  522. if fourccType == "mdat" && boxHeader.Size == 1 {
  523. nextOffset = int64(boxHeader.Size64)
  524. }
  525. if nextOffset == 0 {
  526. return 0, fmt.Errorf("box size is zero, which is invalid and likely means a parsing error")
  527. }
  528. offset += nextOffset
  529. }
  530. // 读取moov盒子数据
  531. moovBoxData, err := readBox(reader, offset, uint64(boxHeader.Size))
  532. if err != nil {
  533. return 0, fmt.Errorf("error reading moov box at offset %d: %w", offset, err)
  534. }
  535. // 在moov盒子中查找mvhd或tkhd盒子
  536. mvhdBox, err := findBox(moovBoxData, "mvhd")
  537. if err != nil {
  538. tkhdBox, err := findBox(moovBoxData, "tkhd")
  539. if err != nil {
  540. return 0, fmt.Errorf("neither mvhd nor tkhd box found in moov box")
  541. }
  542. mvhdBox = tkhdBox
  543. }
  544. // 解析mvhd或tkhd盒子中的timeScale和duration
  545. var timeScaleOffset, durationOffset uint32
  546. if getFourccType(getHeaderBoxInfo(mvhdBox)) == "mvhd" {
  547. timeScaleOffset = 0x1C
  548. durationOffset = 0x20
  549. } else if getFourccType(getHeaderBoxInfo(mvhdBox)) == "tkhd" {
  550. timeScaleOffset = 0x14
  551. durationOffset = 0x18
  552. } else {
  553. return 0, fmt.Errorf("unsupported box type in moov box")
  554. }
  555. timeScale := binary.BigEndian.Uint32(mvhdBox[timeScaleOffset : timeScaleOffset+4])
  556. duration := binary.BigEndian.Uint32(mvhdBox[durationOffset : durationOffset+4])
  557. fmt.Printf("timeScale: %d, duration: %d\n", timeScale, duration)
  558. if timeScale == 0 {
  559. return 0, fmt.Errorf("timeScale is zero, division by zero is not possible")
  560. }
  561. lengthOfTime = int(duration / timeScale)
  562. return
  563. }