index.vue 76 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679
  1. <template>
  2. <div class="push-wrap">
  3. <div
  4. ref="topRef"
  5. class="left"
  6. >
  7. <div
  8. ref="containerRef"
  9. class="container"
  10. >
  11. <div
  12. class="recording"
  13. v-if="recording"
  14. >
  15. ● REC {{ recordVideoTime }}
  16. </div>
  17. <div
  18. class="record-ico"
  19. @click="handleRecordVideo"
  20. >
  21. <n-popover
  22. placement="top"
  23. trigger="hover"
  24. :flip="false"
  25. >
  26. <template #trigger>
  27. <n-icon
  28. size="26"
  29. :color="recording ? 'red' : '#3f7ee8'"
  30. >
  31. <Videocam v-if="!recording"></Videocam>
  32. <VideocamOffSharp v-else></VideocamOffSharp>
  33. </n-icon>
  34. </template>
  35. <div class="slider">{{ !recording ? '开始录制' : '结束录制' }}</div>
  36. </n-popover>
  37. </div>
  38. <canvas
  39. id="pushCanvasRef"
  40. ref="pushCanvasRef"
  41. ></canvas>
  42. <div
  43. v-if="appStore.allTrack.filter((item) => !item.hidden).length <= 0"
  44. class="add-wrap"
  45. >
  46. <n-space>
  47. <n-button
  48. v-for="(item, index) in allMediaTypeList"
  49. :key="index"
  50. class="item"
  51. @click="handleStartMedia(item)"
  52. >
  53. {{ item.txt }}
  54. </n-button>
  55. </n-space>
  56. </div>
  57. </div>
  58. <div
  59. ref="bottomRef"
  60. class="room-control"
  61. >
  62. <div class="info">
  63. <div
  64. class="avatar"
  65. :style="{ backgroundImage: `url(${userStore.userInfo?.avatar})` }"
  66. ></div>
  67. <div class="detail">
  68. <div class="top">
  69. <n-input-group>
  70. <n-input
  71. v-model:value="roomName"
  72. size="small"
  73. placeholder="输入房间名"
  74. :style="{ width: '50%' }"
  75. />
  76. <n-button
  77. size="small"
  78. type="primary"
  79. @click="confirmRoomName"
  80. >
  81. 确定
  82. </n-button>
  83. </n-input-group>
  84. </div>
  85. <div class="bottom">
  86. <!-- <span v-if="NODE_ENV === 'development'"> -->
  87. {{ mySocketId }}
  88. <!-- </span> -->
  89. </div>
  90. </div>
  91. </div>
  92. <div class="rtc">
  93. <div class="item">
  94. <div class="txt">码率设置</div>
  95. <div class="down">
  96. <n-select
  97. v-model:value="currentMaxBitrate"
  98. :options="maxBitrate"
  99. />
  100. </div>
  101. </div>
  102. <div class="item">
  103. <div class="txt">帧率设置</div>
  104. <div class="down">
  105. <n-select
  106. v-model:value="currentMaxFramerate"
  107. :options="maxFramerate"
  108. />
  109. </div>
  110. </div>
  111. <div class="item">
  112. <div class="txt">分辨率设置</div>
  113. <div class="down">
  114. <n-select
  115. v-model:value="currentResolutionRatio"
  116. :options="resolutionRatio"
  117. />
  118. </div>
  119. </div>
  120. </div>
  121. <div class="other">
  122. <div class="top">
  123. <div class="item">
  124. <i class="ico"></i>
  125. <span>
  126. 正在观看:
  127. {{ liveUserList.length }}
  128. </span>
  129. </div>
  130. <div
  131. class="item"
  132. v-if="NODE_ENV === 'development'"
  133. >
  134. {{ liveRoomTypeEnumMap[appStore.liveRoomInfo?.type || ''] }}
  135. </div>
  136. </div>
  137. <div class="bottom">
  138. <n-button
  139. v-if="!roomLiving"
  140. type="info"
  141. size="small"
  142. @click="handleStartLive"
  143. >
  144. 开始直播
  145. </n-button>
  146. <n-button
  147. v-else
  148. type="error"
  149. size="small"
  150. @click="handleEndLive"
  151. >
  152. 结束直播
  153. </n-button>
  154. </div>
  155. </div>
  156. </div>
  157. </div>
  158. <div class="right">
  159. <div class="resource-card">
  160. <div class="title">素材列表</div>
  161. <div class="list">
  162. <div
  163. v-for="(item, index) in appStore.allTrack.filter(
  164. (item) => !item.hidden
  165. )"
  166. :key="index"
  167. class="item"
  168. @click="handleActiveObject(item)"
  169. >
  170. <div class="item-left">
  171. <div
  172. class="control-item"
  173. @click="handleEye(item)"
  174. >
  175. <n-icon
  176. size="16"
  177. v-if="item.openEye && item.type !== MediaTypeEnum.microphone"
  178. >
  179. <EyeOutline></EyeOutline>
  180. </n-icon>
  181. <n-icon
  182. size="16"
  183. v-else
  184. >
  185. <EyeOffOutline></EyeOffOutline>
  186. </n-icon>
  187. </div>
  188. <div class="name">
  189. {{ NODE_ENV === 'development' ? item.id : '' }}({{
  190. mediaTypeEnumMap[item.type]
  191. }}){{ item.mediaName }}
  192. </div>
  193. </div>
  194. <div class="control">
  195. <div
  196. v-if="item.audio === 1"
  197. class="control-item"
  198. @click.stop="handleChangeMuted(item)"
  199. >
  200. <n-popover
  201. placement="top"
  202. trigger="hover"
  203. :flip="false"
  204. >
  205. <template #trigger>
  206. <n-icon size="16">
  207. <VolumeMuteOutline v-if="item.muted"></VolumeMuteOutline>
  208. <VolumeHighOutline v-else></VolumeHighOutline>
  209. </n-icon>
  210. </template>
  211. <div class="slider">
  212. <n-slider
  213. :value="item.volume"
  214. :step="1"
  215. @update-value="(v) => handleChangeVolume(item, v)"
  216. />
  217. </div>
  218. </n-popover>
  219. </div>
  220. <div
  221. class="control-item"
  222. @click="handleEdit(item)"
  223. >
  224. <n-icon size="16">
  225. <CreateOutline></CreateOutline>
  226. </n-icon>
  227. </div>
  228. <div
  229. class="control-item"
  230. @click.stop="handleDel(item)"
  231. >
  232. <n-icon size="16">
  233. <Close></Close>
  234. </n-icon>
  235. </div>
  236. </div>
  237. </div>
  238. </div>
  239. <div class="bottom">
  240. <n-button
  241. size="small"
  242. type="primary"
  243. @click="showSelectMediaModalCpt = true"
  244. >
  245. 添加素材
  246. </n-button>
  247. </div>
  248. </div>
  249. <div class="danmu-card">
  250. <div class="title">弹幕互动</div>
  251. <div class="list-wrap">
  252. <div
  253. ref="danmuListRef"
  254. class="list"
  255. >
  256. <div
  257. v-for="(item, index) in damuList"
  258. :key="index"
  259. class="item"
  260. >
  261. <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
  262. <span class="name">
  263. <span v-if="item.userInfo">
  264. {{ item.userInfo.username }}[{{
  265. item.userInfo.roles?.map((v) => v.role_name).join()
  266. }}]
  267. </span>
  268. <span v-else>{{ item.socket_id }}[游客]</span>
  269. </span>
  270. <span>:</span>
  271. <span
  272. class="msg"
  273. v-if="item.msgIsFile === WsMessageMsgIsFileEnum.no"
  274. >
  275. {{ item.msg }}
  276. </span>
  277. <div
  278. class="msg img"
  279. v-else
  280. >
  281. <img
  282. :src="item.msg"
  283. alt=""
  284. @load="handleScrollTop"
  285. />
  286. </div>
  287. </template>
  288. <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
  289. <span class="name system">系统通知:</span>
  290. <span class="msg">
  291. <span>{{ item.userInfo?.username || item.socket_id }}</span>
  292. <span>进入直播!</span>
  293. </span>
  294. </template>
  295. <template
  296. v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved"
  297. >
  298. <span class="name system">系统通知:</span>
  299. <span class="msg">
  300. <span>{{ item.userInfo?.username || item.socket_id }}</span>
  301. <span>离开直播!</span>
  302. </span>
  303. </template>
  304. </div>
  305. </div>
  306. </div>
  307. <div
  308. class="send-msg"
  309. v-loading="msgLoading"
  310. >
  311. <div class="control">
  312. <div
  313. class="emoji-list"
  314. v-if="showEmoji"
  315. >
  316. <div
  317. class="item"
  318. v-for="(item, index) in emojiArray"
  319. :key="index"
  320. @click="handlePushStr(item)"
  321. >
  322. {{ item }}
  323. </div>
  324. </div>
  325. <div
  326. class="ico face"
  327. title="表情"
  328. @click="showEmoji = !showEmoji"
  329. ></div>
  330. <div
  331. class="ico img"
  332. title="图片"
  333. @click="mockClick"
  334. >
  335. <input
  336. ref="uploadRef"
  337. type="file"
  338. class="input-upload"
  339. accept=".webp,.png,.jpg,.jpeg,.gif"
  340. @change="uploadChange"
  341. />
  342. </div>
  343. </div>
  344. <textarea
  345. v-model="danmuStr"
  346. class="ipt"
  347. @keydown="keydownDanmu"
  348. ></textarea>
  349. <div
  350. class="btn"
  351. @click="handleSendDanmu"
  352. >
  353. 发送
  354. </div>
  355. </div>
  356. <!-- <div class="send-msg">
  357. <input
  358. v-model="danmuStr"
  359. class="ipt"
  360. @keydown="keydownDanmu"
  361. />
  362. <n-button
  363. type="info"
  364. size="small"
  365. @click="sendDanmu"
  366. >
  367. 发送
  368. </n-button>
  369. </div> -->
  370. </div>
  371. </div>
  372. </div>
  373. <div>
  374. <SelectMediaModalCpt
  375. v-if="showSelectMediaModalCpt"
  376. :all-media-type-list="allMediaTypeList"
  377. @close="showSelectMediaModalCpt = false"
  378. @ok="handleShowMediaModalCpt"
  379. ></SelectMediaModalCpt>
  380. <MediaModalCpt
  381. v-if="showMediaModalCpt"
  382. :is-edit="isEdit"
  383. :media-type="currentMediaType"
  384. :init-data="currentMediaData"
  385. @close="showMediaModalCpt = false"
  386. @add-ok="addMediaOk"
  387. @edit-ok="editMediaOk"
  388. ></MediaModalCpt>
  389. <OpenMicophoneTipCpt
  390. v-if="showOpenMicophoneTipCpt"
  391. @close="showOpenMicophoneTipCpt = false"
  392. ></OpenMicophoneTipCpt>
  393. <NoLiveTipModalCpt
  394. v-if="showNoLiveTipModalCpt"
  395. @close="showNoLiveTipModalCpt = false"
  396. ></NoLiveTipModalCpt>
  397. </div>
  398. </template>
  399. <script lang="ts" setup>
  400. import {
  401. Close,
  402. CreateOutline,
  403. EyeOffOutline,
  404. EyeOutline,
  405. Videocam,
  406. VideocamOffSharp,
  407. VolumeHighOutline,
  408. VolumeMuteOutline,
  409. } from '@vicons/ionicons5';
  410. import { AVRecorder } from '@webav/av-recorder';
  411. import { getRandomString } from 'billd-utils';
  412. import { fabric } from 'fabric';
  413. import {
  414. Raw,
  415. markRaw,
  416. onMounted,
  417. onUnmounted,
  418. reactive,
  419. ref,
  420. watch,
  421. } from 'vue';
  422. import { useRoute } from 'vue-router';
  423. import { fetchGetWsMessageList } from '@/api/wsMessage';
  424. import {
  425. QINIU_RESOURCE,
  426. liveRoomTypeEnumMap,
  427. mediaTypeEnumMap,
  428. } from '@/constant';
  429. import { emojiArray } from '@/emoji';
  430. import { commentAuthTip, loginTip } from '@/hooks/use-login';
  431. import { usePush } from '@/hooks/use-push';
  432. import { useRTCParams } from '@/hooks/use-rtcParams';
  433. import { useQiniuJsUpload } from '@/hooks/use-upload';
  434. import {
  435. DanmuMsgTypeEnum,
  436. MediaTypeEnum,
  437. WsMessageMsgIsFileEnum,
  438. WsMessageMsgIsShowEnum,
  439. WsMessageMsgIsVerifyEnum,
  440. } from '@/interface';
  441. import { AppRootState, useAppStore } from '@/store/app';
  442. import { usePiniaCacheStore } from '@/store/cache';
  443. import { useNetworkStore } from '@/store/network';
  444. import { useUserStore } from '@/store/user';
  445. import { LiveRoomTypeEnum } from '@/types/ILiveRoom';
  446. import { WsMsgTypeEnum, WsUpdateLiveRoomCoverImg } from '@/types/websocket';
  447. import {
  448. base64ToFile,
  449. createVideo,
  450. formatDownTime2,
  451. generateBase64,
  452. getRandomEnglishString,
  453. handleUserMedia,
  454. readFile,
  455. saveFile,
  456. } from '@/utils';
  457. import { NODE_ENV } from 'script/constant';
  458. import MediaModalCpt from './mediaModal/index.vue';
  459. import NoLiveTipModalCpt from './noLiveTipModal/index.vue';
  460. import OpenMicophoneTipCpt from './openMicophoneTip/index.vue';
  461. import SelectMediaModalCpt from './selectMediaModal/index.vue';
  462. const route = useRoute();
  463. const userStore = useUserStore();
  464. const appStore = useAppStore();
  465. const networkStore = useNetworkStore();
  466. const cacheStore = usePiniaCacheStore();
  467. const { maxBitrate, maxFramerate, resolutionRatio, allMediaTypeList } =
  468. useRTCParams();
  469. const {
  470. confirmRoomName,
  471. startLive,
  472. endLive,
  473. sendDanmu,
  474. keydownDanmu,
  475. sendBlob,
  476. handleSendGetLiveUser,
  477. roomId,
  478. msgIsFile,
  479. mySocketId,
  480. canvasVideoStream,
  481. roomLiving,
  482. currentResolutionRatio,
  483. currentMaxBitrate,
  484. currentMaxFramerate,
  485. danmuStr,
  486. roomName,
  487. damuList,
  488. liveUserList,
  489. } = usePush();
  490. const currentMediaType = ref(MediaTypeEnum.camera);
  491. const currentMediaData = ref<AppRootState['allTrack'][0]>();
  492. const recording = ref(false);
  493. const showOpenMicophoneTipCpt = ref(false);
  494. const showSelectMediaModalCpt = ref(false);
  495. const showMediaModalCpt = ref(false);
  496. const showNoLiveTipModalCpt = ref(false);
  497. const isEdit = ref(false);
  498. const topRef = ref<HTMLDivElement>();
  499. const bottomRef = ref<HTMLDivElement>();
  500. const danmuListRef = ref<HTMLDivElement>();
  501. const containerRef = ref<HTMLDivElement>();
  502. const pushCanvasRef = ref<HTMLCanvasElement>();
  503. const webaudioVideo = ref<HTMLVideoElement>();
  504. const fabricCanvas = ref<fabric.Canvas>();
  505. const startTime = ref(+new Date());
  506. const initAudioFlag = ref(false);
  507. const msgLoading = ref(false);
  508. const uploadRef = ref<HTMLInputElement>();
  509. const nullAudioStream = ref<MediaStream>();
  510. const showEmoji = ref(false);
  511. const worker = ref<Worker>();
  512. const workerTimerId = ref();
  513. const workerMsrTimerId = ref();
  514. const timeCanvasDom = ref<Raw<fabric.Text>[]>([]);
  515. const stopwatchCanvasDom = ref<Raw<fabric.Text>[]>([]);
  516. const wrapSize = reactive({
  517. width: 0,
  518. height: 0,
  519. });
  520. const bodyAppendChildElArr = ref<HTMLElement[]>([]);
  521. const liveType = Number(route.query.liveType);
  522. const recorder = ref<MediaRecorder>();
  523. const bolbId = ref(0);
  524. const msrDelay = ref(1000 * 1);
  525. const msrMaxDelay = ref(1000 * 5);
  526. const suggestedName = ref('');
  527. const recordVideoTimer = ref();
  528. const recordVideoTime = ref('00:00:00');
  529. let avRecorder: AVRecorder | null = null;
  530. watch(
  531. () => roomLiving.value,
  532. (newval) => {
  533. if (!newval) {
  534. handleEndLive();
  535. showNoLiveTipModalCpt.value = true;
  536. } else {
  537. uploadLivePreview();
  538. }
  539. }
  540. );
  541. watch(
  542. () => currentMaxBitrate.value,
  543. () => {
  544. if (liveType === LiveRoomTypeEnum.msr) {
  545. const stream = pushCanvasRef.value!.captureStream();
  546. const audioTrack = webaudioVideo
  547. // @ts-ignore
  548. .value!.captureStream()
  549. .getAudioTracks()[0];
  550. stream.addTrack(audioTrack);
  551. handleMsr(stream);
  552. }
  553. }
  554. );
  555. watch(
  556. () => currentMaxFramerate.value,
  557. () => {
  558. renderFrame();
  559. }
  560. );
  561. const lockMap = ref(new Set());
  562. watch(
  563. () => networkStore.rtcMap,
  564. (newVal) => {
  565. newVal.forEach((item) => {
  566. // if (appStore.allTrack.find((v) => v.mediaName === item.receiver)) {
  567. // return;
  568. // }
  569. // if (lockMap.value.has(item.localStream?.id)) {
  570. // return;
  571. // }
  572. // if (item.localStream?.id) {
  573. // lockMap.value.add(item.localStream?.id);
  574. // }
  575. addMediaOk({
  576. id: getRandomEnglishString(6),
  577. openEye: true,
  578. audio: 1,
  579. video: 1,
  580. mediaName: item.receiver,
  581. type: MediaTypeEnum.metting,
  582. track: item.localStream?.getVideoTracks()[0],
  583. trackid: item.localStream?.getVideoTracks()[0].id,
  584. stream: item.localStream || undefined,
  585. streamid: item.localStream?.id,
  586. hidden: false,
  587. muted: false,
  588. scaleInfo: {},
  589. });
  590. });
  591. },
  592. {
  593. deep: true,
  594. immediate: true,
  595. }
  596. );
  597. watch(
  598. () => damuList.value.length,
  599. () => {
  600. setTimeout(() => {
  601. handleScrollTop();
  602. }, 0);
  603. }
  604. );
  605. function handleSendDanmu() {
  606. sendDanmu();
  607. }
  608. function handlePushStr(str) {
  609. danmuStr.value += str;
  610. showEmoji.value = false;
  611. }
  612. function handleScrollTop() {
  613. if (danmuListRef.value) {
  614. danmuListRef.value.scrollTop = danmuListRef.value.scrollHeight + 10000;
  615. }
  616. }
  617. function handleSendBlob(event: BlobEvent) {
  618. bolbId.value += 1;
  619. // const val = bolbId.value;
  620. // if (val % 3 === 0) {
  621. // setTimeout(() => {
  622. // sendBlob({
  623. // blob: event.data,
  624. // blobId: `${val}`,
  625. // delay: msrDelay.value,
  626. // max_delay: msrMaxDelay.value,
  627. // });
  628. // }, 1000);
  629. // return;
  630. // }
  631. sendBlob({
  632. blob: event.data,
  633. blobId: `${bolbId.value}`,
  634. delay: msrDelay.value,
  635. max_delay: msrMaxDelay.value,
  636. });
  637. }
  638. function mockClick() {
  639. if (!loginTip()) {
  640. return;
  641. }
  642. if (!commentAuthTip()) {
  643. return;
  644. }
  645. uploadRef.value?.click();
  646. }
  647. async function uploadChange() {
  648. const fileList = uploadRef.value?.files;
  649. if (fileList?.length) {
  650. try {
  651. msgLoading.value = true;
  652. msgIsFile.value = WsMessageMsgIsFileEnum.yes;
  653. const res = await useQiniuJsUpload({
  654. prefix: QINIU_RESOURCE.prefix['billd-live/msg-image/'],
  655. file: fileList[0],
  656. });
  657. if (res?.resultUrl) {
  658. danmuStr.value = res.resultUrl || '错误图片';
  659. sendDanmu();
  660. }
  661. } catch (error) {
  662. console.log(error);
  663. } finally {
  664. msgIsFile.value = WsMessageMsgIsFileEnum.no;
  665. msgLoading.value = false;
  666. if (uploadRef.value) {
  667. uploadRef.value.value = '';
  668. }
  669. }
  670. }
  671. }
  672. function handleMediaRecorderAllType() {
  673. // const types = [
  674. // 'video/webm',
  675. // 'audio/webm',
  676. // 'video/mpeg',
  677. // 'video/webm;codecs=vp8',
  678. // 'video/webm;codecs=vp9',
  679. // 'video/webm;codecs=daala',
  680. // 'video/webm;codecs=h264',
  681. // 'audio/webm;codecs=opus',
  682. // 'audio/webm;codecs=aac',
  683. // 'audio/webm;codecs=h264,opus',
  684. // 'video/webm;codecs=avc1.64001f,opus',
  685. // 'video/webm;codecs=avc1.4d002a,opus',
  686. // ];
  687. // Object.keys(types).forEach((item) => {
  688. // console.log(types[item], MediaRecorder.isTypeSupported(types[item]));
  689. // });
  690. }
  691. function handleMsr(stream: MediaStream) {
  692. // https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter
  693. const mimeType = 'video/webm;codecs=avc1.4d002a,opus';
  694. // const mimeType = 'video/webm;codecs=avc1.64001f,opus'; // b站的参数
  695. if (!MediaRecorder.isTypeSupported(mimeType)) {
  696. console.error('不支持', mimeType);
  697. return;
  698. } else {
  699. console.log('支持', mimeType);
  700. }
  701. /**
  702. * 小写的 "kb/s" 表示千比特每秒,而大写的 "KB/s" 表示千字节每秒
  703. * 例如,当我们说 100 kb/s 时,意思是每秒传输100千比特(比特)的数据。而当我们说 100 KB/s 时,意思是每秒传输100千字节(字节)的数据,相当于800千比特(比特)
  704. * 千字节(KB)、兆字节(MB)、千兆字节(GB)
  705. * 8 比特(bits)等于 1 字节(byte)
  706. * 1 Kbps(千比特每秒)等于 0.125 KB/s(千字节每秒)
  707. * 1 Mbps(兆比特每秒)等于 0.125 MB/s(兆字节每秒)
  708. * bit,比特
  709. * byte,字节
  710. * videoBitsPerSecond的单位是比特,假设videoBitsPerSecond值是1000*2000,即2000000
  711. * 2000000比特等于2000000 / 8 / 1000 = 250 KB/s
  712. */
  713. recorder.value = new MediaRecorder(stream, {
  714. mimeType,
  715. // bitsPerSecond: 1000 * currentMaxBitrate.value,
  716. videoBitsPerSecond: 1000 * currentMaxBitrate.value, // 单位是比特
  717. // audioBitsPerSecond: 1000 * 2000,
  718. });
  719. recorder.value.ondataavailable = handleSendBlob;
  720. worker.value?.postMessage({
  721. type: 'request-clear-loop',
  722. timer: workerMsrTimerId.value,
  723. });
  724. worker.value?.postMessage({
  725. type: 'request-start-msr-loop',
  726. delay: msrDelay.value,
  727. });
  728. worker.value?.addEventListener('message', (e) => {
  729. if (e.data.type === 'response-start-msr-loop') {
  730. workerMsrTimerId.value = e.data.timer;
  731. } else if (e.data.type === 'response-msr-looping') {
  732. recorder.value?.stop();
  733. recorder.value?.start();
  734. }
  735. });
  736. }
  737. watch(
  738. () => roomId.value,
  739. (newval) => {
  740. if (newval) {
  741. handleSendGetLiveUser(Number(newval));
  742. handleHistoryMsg();
  743. }
  744. }
  745. );
  746. onMounted(() => {
  747. worker.value = new Worker('worker.js');
  748. setTimeout(() => {
  749. scrollTo(0, 0);
  750. }, 100);
  751. handleMediaRecorderAllType();
  752. initUserMedia();
  753. initCanvas();
  754. handleCache();
  755. });
  756. onUnmounted(() => {
  757. clearInterval(recordVideoTimer.value);
  758. recorder.value?.stop();
  759. bodyAppendChildElArr.value.forEach((el) => {
  760. el.remove();
  761. });
  762. clearFrame();
  763. worker.value?.terminate();
  764. appStore.allTrack.forEach((v) => {
  765. v.videoEl?.pause();
  766. v.videoEl?.removeAttribute('src');
  767. v.videoEl?.remove();
  768. v.stream?.getTracks().forEach((track) => {
  769. track.stop();
  770. v.stream?.removeTrack(track);
  771. });
  772. });
  773. });
  774. async function initUserMedia() {
  775. const res1 = await handleUserMedia({ video: true, audio: false });
  776. console.log('初始化获取摄像头成功');
  777. const res2 = await handleUserMedia({ video: false, audio: true });
  778. console.log('初始化获取麦克风成功');
  779. if (!res1 || !res2) {
  780. showOpenMicophoneTipCpt.value = true;
  781. }
  782. }
  783. function renderAll() {
  784. timeCanvasDom.value.forEach((item) => {
  785. item.text = new Date().toLocaleString();
  786. });
  787. stopwatchCanvasDom.value.forEach((item) => {
  788. const res = formatDownTime2({
  789. endTime: +new Date(),
  790. startTime: startTime.value,
  791. showMillisecond: true,
  792. addZero: true,
  793. });
  794. item.text = `${res.d}天${res.h}时${res.m}分${res.s}秒${res.ms}毫秒`;
  795. });
  796. fabricCanvas.value?.renderAll();
  797. }
  798. function clearFrame() {
  799. worker.value?.postMessage({
  800. type: 'request-clear-loop',
  801. data: {
  802. timer: workerTimerId.value,
  803. },
  804. });
  805. }
  806. function renderFrame() {
  807. /**
  808. * 理论情况:
  809. * currentMaxFramerate等于20,即每秒20帧,即1000 / 20 = 50毫秒轮询一次
  810. * currentMaxFramerate等于30,即每秒30帧,即1000 / 30 = 33.333毫秒轮询一次
  811. * currentMaxFramerate等于30,即每秒60帧,即1000 / 60 = 16.666毫秒轮询一次
  812. * 实际情况:
  813. * currentMaxFramerate等于20,即50毫秒轮询一次,实际fps是18
  814. * currentMaxFramerate等于20,希望fps是20,即需要(18/20) * 50 = 45毫秒轮询一次
  815. */
  816. let delay = 1000 / currentMaxFramerate.value;
  817. delay = (18 / 20) * delay;
  818. worker.value?.postMessage({
  819. type: 'request-clear-loop',
  820. timer: workerTimerId.value,
  821. });
  822. worker.value?.postMessage({
  823. type: 'request-start-loop',
  824. delay,
  825. });
  826. worker.value?.addEventListener('message', (e) => {
  827. if (e.data.type === 'response-start-loop') {
  828. workerTimerId.value = e.data.timer;
  829. } else if (e.data.type === 'response-looping') {
  830. renderAll();
  831. }
  832. });
  833. }
  834. function handleNullAudio() {
  835. // 创建AudioContext对象
  836. const audioContext = new window.AudioContext();
  837. // 创建输入和输出节点
  838. const source = audioContext.createBufferSource();
  839. const destination = audioContext.createMediaStreamDestination();
  840. // 连接输入和输出节点
  841. source.connect(destination);
  842. // 播放空白音频
  843. source.start();
  844. // 获取音频流
  845. const stream = destination.stream;
  846. // 检查是否已经获取到音频流
  847. if (stream) {
  848. console.log('已创建空的直播音频流');
  849. const video = createVideo({
  850. appendChild: false,
  851. });
  852. video.srcObject = stream;
  853. nullAudioStream.value = stream;
  854. } else {
  855. console.error('无法创建空的直播音频流');
  856. }
  857. }
  858. function handleMixedAudio() {
  859. const allAudioTrack = appStore.allTrack.filter((item) => item.audio === 1);
  860. const nullAudio = nullAudioStream.value?.getAudioTracks()[0];
  861. if (nullAudio) {
  862. allAudioTrack.push({
  863. openEye: true,
  864. id: getRandomEnglishString(6),
  865. audio: 2,
  866. video: 1,
  867. mediaName: '占位空音频',
  868. type: MediaTypeEnum.webAudio,
  869. track: nullAudio,
  870. trackid: nullAudio.id,
  871. stream: nullAudioStream.value,
  872. streamid: nullAudioStream.value?.id,
  873. hidden: false,
  874. muted: false,
  875. scaleInfo: {},
  876. });
  877. }
  878. const audioCtx = new AudioContext();
  879. if (canvasVideoStream.value?.getAudioTracks()[0]) {
  880. canvasVideoStream.value.removeTrack(
  881. canvasVideoStream.value.getAudioTracks()[0]
  882. );
  883. }
  884. const res: { source: MediaStreamAudioSourceNode; gainNode: GainNode }[] = [];
  885. allAudioTrack.forEach((item) => {
  886. if (!audioCtx || !item.stream) return;
  887. const source = audioCtx.createMediaStreamSource(item.stream);
  888. const gainNode = audioCtx.createGain();
  889. gainNode.gain.value = (item.volume || 0) / 100;
  890. source.connect(gainNode);
  891. res.push({ source, gainNode });
  892. // console.log('混流', item.stream?.id, item.stream);
  893. });
  894. const destination = audioCtx.createMediaStreamDestination();
  895. res.forEach((item) => {
  896. item.source.connect(item.gainNode);
  897. item.gainNode.connect(destination);
  898. });
  899. if (webaudioVideo.value) {
  900. webaudioVideo.value.remove();
  901. }
  902. webaudioVideo.value = createVideo({
  903. appendChild: false,
  904. muted: true,
  905. });
  906. bodyAppendChildElArr.value.push(webaudioVideo.value);
  907. webaudioVideo.value.className = 'web-audio-video';
  908. webaudioVideo.value!.srcObject = destination.stream;
  909. const resAudio = destination.stream.getAudioTracks()[0];
  910. canvasVideoStream.value?.addTrack(resAudio);
  911. networkStore.rtcMap.forEach((rtc) => {
  912. const sender = rtc.peerConnection
  913. ?.getSenders()
  914. .find((sender) => sender.track?.id === resAudio.id);
  915. if (!sender) {
  916. rtc.peerConnection
  917. ?.getSenders()
  918. ?.find((sender) => sender.track?.kind === 'audio')
  919. ?.replaceTrack(resAudio);
  920. }
  921. });
  922. }
  923. function handleEndLive() {
  924. worker.value?.postMessage({
  925. type: 'request-clear-loop',
  926. timer: workerMsrTimerId.value,
  927. });
  928. recorder.value?.removeEventListener('dataavailable', handleSendBlob);
  929. endLive();
  930. }
  931. async function handleHistoryMsg() {
  932. try {
  933. const res = await fetchGetWsMessageList({
  934. nowPage: 1,
  935. pageSize: appStore.liveRoomInfo?.history_msg_total || 10,
  936. orderName: 'created_at',
  937. orderBy: 'desc',
  938. live_room_id: Number(roomId.value),
  939. is_show: WsMessageMsgIsShowEnum.yes,
  940. is_verify: WsMessageMsgIsVerifyEnum.yes,
  941. });
  942. if (res.code === 200) {
  943. res.data.rows.forEach((v) => {
  944. damuList.value.unshift({
  945. ...v,
  946. live_room_id: v.live_room_id!,
  947. msg_id: v.id!,
  948. socket_id: '',
  949. msgType: v.msg_type!,
  950. msgIsFile: v.msg_is_file!,
  951. userInfo: v.user,
  952. msg: v.content!,
  953. username: v.username!,
  954. send_msg_time: Number(v.send_msg_time),
  955. redbag_send_id: v.redbag_send_id,
  956. });
  957. });
  958. if (
  959. appStore.liveRoomInfo?.system_msg &&
  960. appStore.liveRoomInfo?.system_msg !== ''
  961. ) {
  962. damuList.value.push({
  963. live_room_id: Number(roomId.value),
  964. socket_id: '',
  965. msgType: DanmuMsgTypeEnum.system,
  966. msgIsFile: WsMessageMsgIsFileEnum.no,
  967. msg: appStore.liveRoomInfo.system_msg,
  968. send_msg_time: Number(+new Date()),
  969. });
  970. }
  971. }
  972. } catch (error) {
  973. console.log(error);
  974. }
  975. }
  976. async function handleRecordVideo() {
  977. if (!window.VideoDecoder || !window.AudioEncoder) {
  978. window.$message.warning(`当前环境不支持录制视频`);
  979. return;
  980. }
  981. initAudio();
  982. try {
  983. if (!recording.value) {
  984. suggestedName.value = `billd直播录制-${+new Date()}.mp4`;
  985. const fileHandle = await window.showSaveFilePicker({
  986. suggestedName: suggestedName.value,
  987. });
  988. const writer = await fileHandle.createWritable();
  989. avRecorder = new AVRecorder(canvasVideoStream.value!.clone(), {});
  990. await avRecorder.start();
  991. const startTime = +new Date();
  992. recordVideoTimer.value = setInterval(() => {
  993. const res = formatDownTime2({
  994. endTime: +new Date(),
  995. startTime,
  996. showMillisecond: true,
  997. addZero: true,
  998. });
  999. if (res.d) {
  1000. recordVideoTime.value = `${res.d}天${res.h}:${res.m}:${res.s}`;
  1001. } else {
  1002. recordVideoTime.value = `${res.h}:${res.m}:${res.s}`;
  1003. }
  1004. }, 1000);
  1005. avRecorder.outputStream?.pipeTo(writer).catch(console.error);
  1006. } else {
  1007. clearInterval(recordVideoTimer.value);
  1008. recordVideoTime.value = '00:00:00';
  1009. await avRecorder?.stop();
  1010. window.$message.success(`录制文件: ${suggestedName.value} 已保存到本地`);
  1011. avRecorder = null;
  1012. }
  1013. recording.value = !recording.value;
  1014. } catch (error) {
  1015. console.log(error);
  1016. recording.value = false;
  1017. }
  1018. }
  1019. function initAudio() {
  1020. if (initAudioFlag.value) return;
  1021. initAudioFlag.value = true;
  1022. handleNullAudio();
  1023. handleMixedAudio();
  1024. }
  1025. async function uploadLivePreview() {
  1026. const base64 = generateBase64(pushCanvasRef.value!);
  1027. const file = base64ToFile(base64, `tmp.webp`);
  1028. const uploadRes = await useQiniuJsUpload({
  1029. prefix: QINIU_RESOURCE.prefix['billd-live/live-preview/'],
  1030. file,
  1031. });
  1032. if (uploadRes.flag && uploadRes.resultUrl) {
  1033. networkStore.wsMap
  1034. .get(roomId.value)
  1035. ?.send<WsUpdateLiveRoomCoverImg['data']>({
  1036. requestId: getRandomString(8),
  1037. msgType: WsMsgTypeEnum.updateLiveRoomCoverImg,
  1038. data: {
  1039. cover_img: uploadRes.resultUrl,
  1040. },
  1041. });
  1042. }
  1043. }
  1044. function handleStartLive() {
  1045. if (!appStore.allTrack.length) {
  1046. window.$message.warning('至少选择一个素材');
  1047. return;
  1048. }
  1049. initAudio();
  1050. startLive({
  1051. type: liveType,
  1052. msrDelay: msrDelay.value,
  1053. msrMaxDelay: 5000,
  1054. });
  1055. if (liveType === LiveRoomTypeEnum.msr) {
  1056. const stream = pushCanvasRef.value!.captureStream();
  1057. // @ts-ignore
  1058. const audioTrack = webaudioVideo.value!.captureStream().getAudioTracks()[0];
  1059. stream.addTrack(audioTrack);
  1060. handleMsr(stream);
  1061. }
  1062. }
  1063. function handleScale({ width, height }: { width: number; height: number }) {
  1064. const resolutionHeight = currentResolutionRatio.value;
  1065. const resolutionWidth = currentResolutionRatio.value * appStore.videoRatio;
  1066. let ratio = 1;
  1067. if (width > resolutionWidth) {
  1068. const r1 = resolutionWidth / width;
  1069. ratio = r1;
  1070. }
  1071. if (height > resolutionHeight) {
  1072. const r1 = resolutionHeight / height;
  1073. if (ratio > r1) {
  1074. ratio = r1;
  1075. }
  1076. }
  1077. return ratio;
  1078. }
  1079. function autoCreateVideo(data: {
  1080. file?: File;
  1081. stream?: MediaStream;
  1082. id: string;
  1083. rect?: { left: number; top: number };
  1084. scaleInfo?: Record<number, { scaleX: number; scaleY: number }>;
  1085. muted?: boolean;
  1086. }) {
  1087. const { file, id, rect, scaleInfo, muted } = data;
  1088. let stream = data.stream;
  1089. let videoEl: HTMLVideoElement;
  1090. if (file) {
  1091. const url = URL.createObjectURL(file);
  1092. videoEl = createVideo({
  1093. appendChild: false,
  1094. muted,
  1095. });
  1096. videoEl.src = url;
  1097. // @ts-ignore
  1098. stream = videoEl.captureStream();
  1099. } else {
  1100. videoEl = createVideo({ appendChild: false, muted });
  1101. if (stream) {
  1102. videoEl.srcObject = stream;
  1103. }
  1104. }
  1105. bodyAppendChildElArr.value.push(videoEl);
  1106. videoEl.setAttribute('videoid', id);
  1107. return new Promise<{
  1108. canvasDom: Raw<fabric.Image>;
  1109. videoEl: HTMLVideoElement;
  1110. scale: number;
  1111. stream: MediaStream;
  1112. }>((resolve) => {
  1113. videoEl.onloadedmetadata = () => {
  1114. let canvasDom: Raw<fabric.Image>;
  1115. let ratio;
  1116. function main() {
  1117. const width = stream?.getVideoTracks()[0].getSettings().width!;
  1118. const height = stream?.getVideoTracks()[0].getSettings().height!;
  1119. ratio = handleScale({ width, height });
  1120. videoEl.width = width;
  1121. videoEl.height = height;
  1122. if (canvasDom) {
  1123. fabricCanvas.value?.remove(canvasDom);
  1124. canvasDom = markRaw(
  1125. new fabric.Image(videoEl, {
  1126. top: (rect?.top || 0) / window.devicePixelRatio,
  1127. left: (rect?.left || 0) / window.devicePixelRatio,
  1128. width,
  1129. height,
  1130. })
  1131. );
  1132. } else {
  1133. canvasDom = markRaw(
  1134. new fabric.Image(videoEl, {
  1135. top: (rect?.top || 0) / window.devicePixelRatio,
  1136. left: (rect?.left || 0) / window.devicePixelRatio,
  1137. width,
  1138. height,
  1139. })
  1140. );
  1141. }
  1142. appStore.allTrack.forEach((item) => {
  1143. if (item.id === id) {
  1144. if (item.canvasDom) {
  1145. item.canvasDom = canvasDom;
  1146. }
  1147. }
  1148. });
  1149. handleMoving({ canvasDom, id });
  1150. handleScaling({ canvasDom, id });
  1151. canvasDom.scale(scaleInfo?.[window.devicePixelRatio]?.scaleX || 1);
  1152. fabricCanvas.value!.add(canvasDom);
  1153. resolve({ canvasDom, scale: ratio, videoEl, stream: stream! });
  1154. }
  1155. main();
  1156. videoEl?.addEventListener('resize', () => {
  1157. main();
  1158. });
  1159. };
  1160. });
  1161. }
  1162. watch(
  1163. () => currentResolutionRatio.value,
  1164. (newHeight, oldHeight) => {
  1165. changeCanvasAttr({ newHeight, oldHeight });
  1166. }
  1167. );
  1168. // 容器宽高,1280*720,即720p
  1169. // canvas容器宽高,2560*1440,即1440p
  1170. // ======
  1171. // 容器宽高,960*540,即540p
  1172. // dom宽高,640*480
  1173. // canvas容器宽高,960*540,即540p
  1174. // 将dom绘制到容器里,此时dom的大小就是640*480
  1175. // 需求,不管切换多少分辨率,我要看到的dom都是一样大小,即
  1176. // 960*540时,dom是640*480
  1177. // 1280*720时,dom不能是640*480了,因为这样他就会对比上一个分辨率的dom看起来小了,960/1280=0.75,540/720=0.75,
  1178. // 其实就是分辨率变大了,我们就要将图片也变大,即图片的宽是640/0.75=853.4,高是480/0.75=640
  1179. // 坐标变化,960*540时,dom坐标是100,100
  1180. // 1280*720时,dom的坐标不能再是100,100了,否则对比上一个分辨率看起来偏
  1181. function changeCanvasAttr({
  1182. newHeight,
  1183. oldHeight,
  1184. }: {
  1185. newHeight: number;
  1186. oldHeight: number;
  1187. }) {
  1188. if (fabricCanvas.value) {
  1189. const resolutionHeight =
  1190. currentResolutionRatio.value / window.devicePixelRatio;
  1191. const resolutionWidth =
  1192. (currentResolutionRatio.value / window.devicePixelRatio) *
  1193. appStore.videoRatio;
  1194. fabricCanvas.value.setWidth(resolutionWidth);
  1195. fabricCanvas.value.setHeight(resolutionHeight);
  1196. appStore.allTrack.forEach((iten) => {
  1197. const item = iten.canvasDom;
  1198. if (item) {
  1199. // 分辨率变小了,将图片变小
  1200. if (newHeight < oldHeight) {
  1201. const ratio2 = oldHeight / newHeight;
  1202. item.left = item.left! / ratio2;
  1203. item.top = item.top! / ratio2;
  1204. } else {
  1205. // 分辨率变大了,将图片变大
  1206. const ratio2 = oldHeight / newHeight;
  1207. item.left = item.left! / ratio2;
  1208. item.top = item.top! / ratio2;
  1209. }
  1210. }
  1211. });
  1212. appStore.allTrack.forEach((iten) => {
  1213. const item = iten.canvasDom;
  1214. if (item) {
  1215. // 分辨率变小了,将图片变小
  1216. if (newHeight < oldHeight) {
  1217. const ratio = newHeight / oldHeight;
  1218. const ratio1 = (item.scaleX || 1) * ratio;
  1219. item.scale(ratio1);
  1220. } else {
  1221. // 分辨率变大了,将图片变大
  1222. const ratio = newHeight / oldHeight;
  1223. const ratio1 = (item.scaleX || 1) * ratio;
  1224. item.scale(ratio1);
  1225. }
  1226. }
  1227. });
  1228. changeCanvasStyle();
  1229. }
  1230. }
  1231. function changeCanvasStyle() {
  1232. // @ts-ignore
  1233. fabricCanvas.value.wrapperEl.style.width = `${wrapSize.width}px`;
  1234. // @ts-ignore
  1235. fabricCanvas.value.wrapperEl.style.height = `${wrapSize.height}px`;
  1236. // @ts-ignore
  1237. fabricCanvas.value.lowerCanvasEl.style.width = `${wrapSize.width}px`;
  1238. // @ts-ignore
  1239. fabricCanvas.value.lowerCanvasEl.style.height = `${wrapSize.height}px`;
  1240. // @ts-ignore
  1241. fabricCanvas.value.upperCanvasEl.style.width = `${wrapSize.width}px`;
  1242. // @ts-ignore
  1243. fabricCanvas.value.upperCanvasEl.style.height = `${wrapSize.height}px`;
  1244. }
  1245. function initCanvas() {
  1246. const resolutionHeight =
  1247. currentResolutionRatio.value / window.devicePixelRatio;
  1248. const resolutionWidth =
  1249. (currentResolutionRatio.value / window.devicePixelRatio) *
  1250. appStore.videoRatio;
  1251. const wrapWidth = containerRef.value!.getBoundingClientRect().width;
  1252. // const wrapWidth = 1920;
  1253. const ratio = wrapWidth / resolutionWidth;
  1254. const wrapHeight = resolutionHeight * ratio;
  1255. // const wrapHeight = 1080;
  1256. // lower-canvas: 实际的canvas画面,也就是pushCanvasRef
  1257. // upper-canvas: 操作时候的canvas
  1258. const ins = markRaw(new fabric.Canvas(pushCanvasRef.value!));
  1259. ins.setWidth(resolutionWidth);
  1260. ins.setHeight(resolutionHeight);
  1261. ins.setBackgroundColor('black', () => {
  1262. console.log('setBackgroundColor回调');
  1263. });
  1264. wrapSize.width = wrapWidth;
  1265. wrapSize.height = wrapHeight;
  1266. fabricCanvas.value = ins;
  1267. renderFrame();
  1268. changeCanvasStyle();
  1269. }
  1270. /**
  1271. * 1: {scaleX: 1, scaleY: 1}
  1272. * 2: {scaleX: 0.5, scaleY: 0.5}
  1273. * 3: {scaleX: 0.3333333333333333, scaleY: 0.3333333333333333}
  1274. * 4: {scaleX: 0.25, scaleY: 0.25}
  1275. */
  1276. /**
  1277. * 二倍屏即1px里面有2个像素;三倍屏1px里面有3个像素,以此类推
  1278. * 一个图片,宽高都是100px
  1279. * 一倍屏展示:100px等于100个像素,一比一展示
  1280. * 二倍屏展示:100px等于100个像素,二比一展示,即在二倍屏的100px看起来会比一倍屏的100px小一倍
  1281. * 如果需要在一杯和二倍屏幕的时候看的大小都一样:
  1282. * 1,在二倍屏的时候,需要将100px放大一倍,即200px;
  1283. * 2,在一倍屏的时候,需要将100px缩小一倍,即50px;
  1284. */
  1285. function handleScaling({ canvasDom, id }) {
  1286. canvasDom.on('scaling', () => {
  1287. appStore.allTrack.forEach((item) => {
  1288. if (id === item.id) {
  1289. item.scaleInfo[window.devicePixelRatio] = {
  1290. scaleX: canvasDom.scaleX || 1,
  1291. scaleY: canvasDom.scaleY || 1,
  1292. };
  1293. Object.keys(item.scaleInfo).forEach((iten) => {
  1294. if (window.devicePixelRatio !== Number(iten)) {
  1295. if (window.devicePixelRatio > Number(iten)) {
  1296. item.scaleInfo[iten] = {
  1297. scaleX:
  1298. item.scaleInfo[window.devicePixelRatio].scaleX *
  1299. window.devicePixelRatio,
  1300. scaleY:
  1301. item.scaleInfo[window.devicePixelRatio].scaleY *
  1302. window.devicePixelRatio,
  1303. };
  1304. } else {
  1305. if (window.devicePixelRatio === 1) {
  1306. item.scaleInfo[iten] = {
  1307. scaleX: item.scaleInfo[1].scaleX / Number(iten),
  1308. scaleY: item.scaleInfo[1].scaleY / Number(iten),
  1309. };
  1310. } else {
  1311. item.scaleInfo[iten] = {
  1312. scaleX: item.scaleInfo[1].scaleX * Number(iten),
  1313. scaleY: item.scaleInfo[1].scaleY * Number(iten),
  1314. };
  1315. }
  1316. }
  1317. }
  1318. });
  1319. }
  1320. });
  1321. cacheStore.setResourceList(appStore.allTrack);
  1322. });
  1323. }
  1324. function handleMoving({
  1325. canvasDom,
  1326. id,
  1327. }: {
  1328. canvasDom: fabric.Image | fabric.Text;
  1329. id: string;
  1330. }) {
  1331. canvasDom.on('moving', () => {
  1332. appStore.allTrack.forEach((item) => {
  1333. if (id === item.id) {
  1334. item.rect = {
  1335. top: (canvasDom.top || 0) * window.devicePixelRatio,
  1336. left: (canvasDom.left || 0) * window.devicePixelRatio,
  1337. };
  1338. }
  1339. });
  1340. cacheStore.setResourceList(appStore.allTrack);
  1341. });
  1342. }
  1343. async function handleDisplayMedia({ video, audio }) {
  1344. try {
  1345. const event = await navigator.mediaDevices.getDisplayMedia({
  1346. video,
  1347. audio,
  1348. });
  1349. return event;
  1350. } catch (error) {
  1351. console.log(error);
  1352. }
  1353. }
  1354. async function handleCache() {
  1355. const res: AppRootState['allTrack'] = [];
  1356. const err: string[] = [];
  1357. const queue: any[] = [];
  1358. cacheStore['resource-list'].forEach((item) => {
  1359. // @ts-ignore
  1360. const obj: AppRootState['allTrack'][0] = {};
  1361. obj.openEye = item.openEye;
  1362. obj.audio = item.audio;
  1363. obj.video = item.video;
  1364. obj.id = item.id;
  1365. obj.deviceId = item.deviceId;
  1366. obj.type = item.type;
  1367. obj.hidden = item.hidden;
  1368. obj.mediaName = item.mediaName;
  1369. obj.muted = item.muted;
  1370. obj.volume = item.volume;
  1371. obj.rect = item.rect;
  1372. obj.scaleInfo = item.scaleInfo;
  1373. obj.stopwatchInfo = item.stopwatchInfo;
  1374. async function handleMediaVideo() {
  1375. const { code, file } = await readFile(item.id);
  1376. if (code === 1 && file) {
  1377. const { videoEl, stream, canvasDom } = await autoCreateVideo({
  1378. file,
  1379. id: obj.id,
  1380. muted: item.muted,
  1381. rect: item.rect,
  1382. scaleInfo: item.scaleInfo,
  1383. });
  1384. if (obj.volume !== undefined) {
  1385. videoEl.volume = obj.volume / 100;
  1386. }
  1387. obj.videoEl = videoEl;
  1388. obj.canvasDom = canvasDom;
  1389. obj.stream = stream;
  1390. obj.streamid = stream.id;
  1391. obj.track = stream.getVideoTracks()[0];
  1392. obj.trackid = stream.getVideoTracks()[0].id;
  1393. } else {
  1394. console.error('读取文件失败');
  1395. }
  1396. }
  1397. async function handleImg() {
  1398. const { code, file } = await readFile(item.id);
  1399. if (code === 1 && file) {
  1400. const imgEl = await new Promise<HTMLImageElement>((resolve) => {
  1401. const reader = new FileReader();
  1402. reader.addEventListener(
  1403. 'load',
  1404. function () {
  1405. const img = document.createElement('img');
  1406. img.src = reader.result as string;
  1407. img.onload = () => {
  1408. resolve(img);
  1409. };
  1410. },
  1411. false
  1412. );
  1413. if (file) {
  1414. reader.readAsDataURL(file);
  1415. }
  1416. });
  1417. if (fabricCanvas.value) {
  1418. const canvasDom = markRaw(
  1419. new fabric.Image(imgEl, {
  1420. top: (item.rect?.top || 0) / window.devicePixelRatio,
  1421. left: (item.rect?.left || 0) / window.devicePixelRatio,
  1422. width: imgEl.width,
  1423. height: imgEl.height,
  1424. })
  1425. );
  1426. handleMoving({ canvasDom, id: obj.id });
  1427. handleScaling({ canvasDom, id: obj.id });
  1428. canvasDom.scale(
  1429. item.scaleInfo?.[window.devicePixelRatio]?.scaleX || 1
  1430. );
  1431. canvasDom.opacity = item.openEye ? 1 : 0;
  1432. fabricCanvas.value.add(canvasDom);
  1433. obj.canvasDom = canvasDom;
  1434. }
  1435. } else {
  1436. console.error('读取文件失败');
  1437. }
  1438. }
  1439. async function handleScreen() {
  1440. try {
  1441. const event = await handleDisplayMedia({
  1442. video: true,
  1443. audio: true,
  1444. });
  1445. if (!event) return;
  1446. const videoEl = createVideo({ appendChild: false });
  1447. bodyAppendChildElArr.value.push(videoEl);
  1448. videoEl.setAttribute('videoid', obj.id);
  1449. videoEl.srcObject = event;
  1450. await new Promise((resolve) => {
  1451. videoEl.onloadedmetadata = () => {
  1452. const stream = videoEl
  1453. // @ts-ignore
  1454. .captureStream();
  1455. const width = stream.getVideoTracks()[0].getSettings().width!;
  1456. const height = stream.getVideoTracks()[0].getSettings().height!;
  1457. videoEl.width = width;
  1458. videoEl.height = height;
  1459. const canvasDom = markRaw(
  1460. new fabric.Image(videoEl, {
  1461. top: (item.rect?.top || 0) / window.devicePixelRatio,
  1462. left: (item.rect?.left || 0) / window.devicePixelRatio,
  1463. width,
  1464. height,
  1465. })
  1466. );
  1467. handleMoving({ canvasDom, id: item.id });
  1468. handleScaling({ canvasDom, id: item.id });
  1469. canvasDom.scale(
  1470. item.scaleInfo?.[window.devicePixelRatio]?.scaleX || 1
  1471. );
  1472. canvasDom.opacity = item.openEye ? 1 : 0;
  1473. fabricCanvas.value!.add(canvasDom);
  1474. obj.videoEl = videoEl;
  1475. obj.canvasDom = canvasDom;
  1476. resolve({ videoEl, canvasDom });
  1477. };
  1478. });
  1479. } catch (error) {
  1480. console.error(error);
  1481. handleDel(obj);
  1482. err.push(obj.id);
  1483. }
  1484. }
  1485. async function handleMicrophone() {
  1486. const event = await handleUserMedia({
  1487. video: false,
  1488. audio: { deviceId: obj.deviceId },
  1489. });
  1490. if (!event) return;
  1491. const videoEl = createVideo({ appendChild: false, muted: true });
  1492. bodyAppendChildElArr.value.push(videoEl);
  1493. videoEl.setAttribute('videoid', obj.id);
  1494. videoEl.srcObject = event;
  1495. if (obj.volume !== undefined) {
  1496. videoEl.volume = obj.muted ? 0 : obj.volume / 100;
  1497. }
  1498. obj.videoEl = videoEl;
  1499. obj.stream = event;
  1500. obj.streamid = event.id;
  1501. obj.track = event.getAudioTracks()[0];
  1502. obj.trackid = event.getAudioTracks()[0].id;
  1503. }
  1504. async function handleCamera() {
  1505. const event = await navigator.mediaDevices.getUserMedia({
  1506. video: { deviceId: obj.deviceId },
  1507. audio: false,
  1508. });
  1509. const videoEl = createVideo({ appendChild: false });
  1510. bodyAppendChildElArr.value.push(videoEl);
  1511. videoEl.setAttribute('videoid', obj.id);
  1512. videoEl.srcObject = event;
  1513. await new Promise((resolve) => {
  1514. videoEl.onloadedmetadata = () => {
  1515. const stream = videoEl
  1516. // @ts-ignore
  1517. .captureStream();
  1518. const width = stream.getVideoTracks()[0].getSettings().width!;
  1519. const height = stream.getVideoTracks()[0].getSettings().height!;
  1520. videoEl.width = width;
  1521. videoEl.height = height;
  1522. const canvasDom = markRaw(
  1523. new fabric.Image(videoEl, {
  1524. top: (item.rect?.top || 0) / window.devicePixelRatio,
  1525. left: (item.rect?.left || 0) / window.devicePixelRatio,
  1526. width,
  1527. height,
  1528. })
  1529. );
  1530. handleMoving({ canvasDom, id: item.id });
  1531. handleScaling({ canvasDom, id: item.id });
  1532. canvasDom.scale(
  1533. item.scaleInfo?.[window.devicePixelRatio]?.scaleX || 1
  1534. );
  1535. canvasDom.opacity = item.openEye ? 1 : 0;
  1536. fabricCanvas.value!.add(canvasDom);
  1537. obj.videoEl = videoEl;
  1538. obj.canvasDom = canvasDom;
  1539. resolve({ videoEl, canvasDom });
  1540. };
  1541. });
  1542. }
  1543. if ([MediaTypeEnum.metting, MediaTypeEnum.pk].includes(obj.type)) {
  1544. err.push(obj.id);
  1545. }
  1546. if (item.type === MediaTypeEnum.media && item.video === 1) {
  1547. queue.push(handleMediaVideo());
  1548. } else if (item.type === MediaTypeEnum.screen) {
  1549. queue.push(handleScreen());
  1550. } else if (item.type === MediaTypeEnum.camera) {
  1551. queue.push(handleCamera());
  1552. } else if (item.type === MediaTypeEnum.microphone) {
  1553. queue.push(handleMicrophone());
  1554. } else if (item.type === MediaTypeEnum.img) {
  1555. queue.push(handleImg());
  1556. } else if (item.type === MediaTypeEnum.txt) {
  1557. obj.txtInfo = item.txtInfo;
  1558. obj.scaleInfo = item.scaleInfo;
  1559. if (fabricCanvas.value) {
  1560. const canvasDom = markRaw(
  1561. new fabric.Text(item.txtInfo?.txt || '', {
  1562. top: (item.rect?.top || 0) / window.devicePixelRatio,
  1563. left: (item.rect?.left || 0) / window.devicePixelRatio,
  1564. fill: item.txtInfo?.color,
  1565. })
  1566. );
  1567. handleMoving({ canvasDom, id: obj.id });
  1568. handleScaling({ canvasDom, id: obj.id });
  1569. // fabric.Text类型不能除以分辨率
  1570. canvasDom.scale(item.scaleInfo?.[window.devicePixelRatio]?.scaleX || 1);
  1571. canvasDom.opacity = item.openEye ? 1 : 0;
  1572. fabricCanvas.value.add(canvasDom);
  1573. obj.canvasDom = canvasDom;
  1574. }
  1575. } else if (item.type === MediaTypeEnum.time) {
  1576. obj.timeInfo = item.timeInfo;
  1577. obj.scaleInfo = item.scaleInfo;
  1578. if (fabricCanvas.value) {
  1579. const canvasDom = markRaw(
  1580. new fabric.Text(new Date().toLocaleString(), {
  1581. top: (item.rect?.top || 0) / window.devicePixelRatio,
  1582. left: (item.rect?.left || 0) / window.devicePixelRatio,
  1583. fill: item.timeInfo?.color,
  1584. })
  1585. );
  1586. timeCanvasDom.value.push(canvasDom);
  1587. handleMoving({ canvasDom, id: obj.id });
  1588. handleScaling({ canvasDom, id: obj.id });
  1589. // fabric.Text类型不能除以分辨率
  1590. canvasDom.scale(item.scaleInfo?.[window.devicePixelRatio]?.scaleX || 1);
  1591. canvasDom.opacity = item.openEye ? 1 : 0;
  1592. fabricCanvas.value.add(canvasDom);
  1593. obj.canvasDom = canvasDom;
  1594. }
  1595. } else if (item.type === MediaTypeEnum.stopwatch) {
  1596. obj.stopwatchInfo = item.stopwatchInfo;
  1597. obj.scaleInfo = item.scaleInfo;
  1598. if (fabricCanvas.value) {
  1599. const canvasDom = markRaw(
  1600. new fabric.Text('00天00时00分00秒000毫秒', {
  1601. top: (item.rect?.top || 0) / window.devicePixelRatio,
  1602. left: (item.rect?.left || 0) / window.devicePixelRatio,
  1603. fill: item.stopwatchInfo?.color,
  1604. })
  1605. );
  1606. stopwatchCanvasDom.value.push(canvasDom);
  1607. handleMoving({ canvasDom, id: obj.id });
  1608. handleScaling({ canvasDom, id: obj.id });
  1609. // fabric.Text类型不能除以分辨率
  1610. canvasDom.scale(item.scaleInfo?.[window.devicePixelRatio]?.scaleX || 1);
  1611. canvasDom.opacity = item.openEye ? 1 : 0;
  1612. fabricCanvas.value.add(canvasDom);
  1613. obj.canvasDom = canvasDom;
  1614. }
  1615. }
  1616. res.push(obj);
  1617. });
  1618. await Promise.all(queue);
  1619. canvasVideoStream.value = pushCanvasRef.value!.captureStream();
  1620. appStore.setAllTrack(res.filter((v) => !err.includes(v.id)));
  1621. }
  1622. function handleShowMediaModalCpt(val: MediaTypeEnum) {
  1623. isEdit.value = false;
  1624. currentMediaData.value = undefined;
  1625. showMediaModalCpt.value = true;
  1626. showSelectMediaModalCpt.value = false;
  1627. currentMediaType.value = val;
  1628. }
  1629. function handleEdit(item: AppRootState['allTrack'][0]) {
  1630. currentMediaType.value = item.type;
  1631. currentMediaData.value = item;
  1632. isEdit.value = true;
  1633. showMediaModalCpt.value = true;
  1634. }
  1635. function setScaleInfo({ track, canvasDom, scale = 1 }) {
  1636. [1, 2, 3, 4].forEach((devicePixelRatio) => {
  1637. track.scaleInfo[devicePixelRatio] = {
  1638. scaleX: (1 / devicePixelRatio) * scale,
  1639. scaleY: (1 / devicePixelRatio) * scale,
  1640. };
  1641. });
  1642. if (window.devicePixelRatio !== 1) {
  1643. const ratio = (1 / window.devicePixelRatio) * scale;
  1644. canvasDom.scale(ratio);
  1645. track.scaleInfo[window.devicePixelRatio] = {
  1646. scaleX: ratio,
  1647. scaleY: ratio,
  1648. };
  1649. }
  1650. }
  1651. async function addMediaOk(val: AppRootState['allTrack'][0]) {
  1652. showMediaModalCpt.value = false;
  1653. if (val.type === MediaTypeEnum.screen) {
  1654. const event = await handleDisplayMedia({
  1655. video: {
  1656. deviceId: val.deviceId,
  1657. // displaySurface: 'monitor', // browser默认标签页;window默认窗口;monitor默认整个屏幕
  1658. },
  1659. audio: true,
  1660. });
  1661. if (!event) return;
  1662. const videoTrack: AppRootState['allTrack'][0] = {
  1663. id: getRandomEnglishString(6),
  1664. openEye: true,
  1665. audio: 2,
  1666. video: 1,
  1667. mediaName: val.mediaName,
  1668. type: MediaTypeEnum.screen,
  1669. track: event.getVideoTracks()[0],
  1670. trackid: event.getVideoTracks()[0].id,
  1671. stream: event,
  1672. streamid: event.id,
  1673. hidden: false,
  1674. muted: false,
  1675. scaleInfo: {},
  1676. rect: { left: 0, top: 0 },
  1677. };
  1678. const { canvasDom, videoEl, scale } = await autoCreateVideo({
  1679. stream: event,
  1680. id: videoTrack.id,
  1681. rect: videoTrack.rect,
  1682. scaleInfo: videoTrack.scaleInfo,
  1683. });
  1684. setScaleInfo({ canvasDom, track: videoTrack, scale });
  1685. videoTrack.videoEl = videoEl;
  1686. videoTrack.canvasDom = canvasDom;
  1687. const audio = event.getAudioTracks();
  1688. if (audio.length) {
  1689. videoTrack.audio = 1;
  1690. videoTrack.volume = appStore.normalVolume;
  1691. const audioTrack: AppRootState['allTrack'][0] = {
  1692. openEye: true,
  1693. id: videoTrack.id,
  1694. audio: 1,
  1695. video: 2,
  1696. mediaName: val.mediaName,
  1697. type: MediaTypeEnum.screen,
  1698. track: event.getAudioTracks()[0],
  1699. trackid: event.getAudioTracks()[0].id,
  1700. stream: event,
  1701. streamid: event.id,
  1702. hidden: true,
  1703. muted: false,
  1704. volume: videoTrack.volume,
  1705. scaleInfo: {},
  1706. };
  1707. const res = [...appStore.allTrack, videoTrack, audioTrack];
  1708. appStore.setAllTrack(res);
  1709. cacheStore.setResourceList(res);
  1710. handleMixedAudio();
  1711. } else {
  1712. const res = [...appStore.allTrack, videoTrack];
  1713. appStore.setAllTrack(res);
  1714. cacheStore.setResourceList(res);
  1715. }
  1716. console.log('获取窗口成功');
  1717. } else if (val.type === MediaTypeEnum.camera) {
  1718. const event = await handleUserMedia({
  1719. video: {
  1720. deviceId: val.deviceId,
  1721. },
  1722. audio: false,
  1723. });
  1724. if (!event) return;
  1725. const videoTrack: AppRootState['allTrack'][0] = {
  1726. id: getRandomEnglishString(6),
  1727. openEye: true,
  1728. deviceId: val.deviceId,
  1729. audio: 2,
  1730. video: 1,
  1731. mediaName: val.mediaName,
  1732. type: MediaTypeEnum.camera,
  1733. track: event.getVideoTracks()[0],
  1734. trackid: event.getVideoTracks()[0].id,
  1735. stream: event,
  1736. streamid: event.id,
  1737. hidden: false,
  1738. muted: false,
  1739. scaleInfo: {},
  1740. rect: { left: 0, top: 0 },
  1741. };
  1742. const { canvasDom, videoEl, scale } = await autoCreateVideo({
  1743. stream: event,
  1744. id: videoTrack.id,
  1745. rect: videoTrack.rect,
  1746. scaleInfo: videoTrack.scaleInfo,
  1747. });
  1748. setScaleInfo({ canvasDom, track: videoTrack, scale });
  1749. videoTrack.videoEl = videoEl;
  1750. videoTrack.canvasDom = canvasDom;
  1751. const res = [...appStore.allTrack, videoTrack];
  1752. appStore.setAllTrack(res);
  1753. cacheStore.setResourceList(res);
  1754. console.log('获取摄像头成功');
  1755. } else if (val.type === MediaTypeEnum.pk) {
  1756. const event = val.stream;
  1757. if (!event) return;
  1758. const videoTrack: AppRootState['allTrack'][0] = {
  1759. id: getRandomEnglishString(6),
  1760. openEye: true,
  1761. deviceId: val.deviceId,
  1762. audio: 2,
  1763. video: 1,
  1764. mediaName: val.mediaName,
  1765. type: MediaTypeEnum.pk,
  1766. track: event.getVideoTracks()[0],
  1767. trackid: event.getVideoTracks()[0].id,
  1768. stream: event,
  1769. streamid: event.id,
  1770. hidden: false,
  1771. muted: false,
  1772. scaleInfo: {},
  1773. rect: { left: 0, top: 0 },
  1774. };
  1775. const { canvasDom, videoEl, scale } = await autoCreateVideo({
  1776. stream: event,
  1777. id: videoTrack.id,
  1778. rect: videoTrack.rect,
  1779. scaleInfo: videoTrack.scaleInfo,
  1780. });
  1781. setScaleInfo({ canvasDom, track: videoTrack, scale });
  1782. videoTrack.videoEl = videoEl;
  1783. videoTrack.canvasDom = canvasDom;
  1784. const res = [...appStore.allTrack, videoTrack];
  1785. appStore.setAllTrack(res);
  1786. cacheStore.setResourceList(res);
  1787. console.log('获取pk成功');
  1788. } else if (val.type === MediaTypeEnum.metting) {
  1789. const event = val.stream;
  1790. if (!event) return;
  1791. const videoTrack = val;
  1792. videoTrack.rect = { left: 0, top: 0 };
  1793. const { canvasDom, videoEl, scale } = await autoCreateVideo({
  1794. stream: event,
  1795. id: videoTrack.id,
  1796. rect: videoTrack.rect,
  1797. scaleInfo: videoTrack.scaleInfo,
  1798. });
  1799. setScaleInfo({ canvasDom, track: videoTrack, scale });
  1800. videoTrack.videoEl = videoEl;
  1801. videoTrack.canvasDom = canvasDom;
  1802. const res = [...appStore.allTrack, videoTrack];
  1803. appStore.setAllTrack(res);
  1804. cacheStore.setResourceList(res);
  1805. console.log('获取会议成功');
  1806. } else if (val.type === MediaTypeEnum.microphone) {
  1807. const event = await handleUserMedia({
  1808. video: false,
  1809. audio: { deviceId: val.deviceId },
  1810. });
  1811. if (!event) return;
  1812. const microphoneVideoTrack: AppRootState['allTrack'][0] = {
  1813. id: getRandomEnglishString(6),
  1814. openEye: true,
  1815. deviceId: val.deviceId,
  1816. audio: 1,
  1817. video: 2,
  1818. mediaName: val.mediaName,
  1819. type: MediaTypeEnum.microphone,
  1820. track: event.getAudioTracks()[0],
  1821. trackid: event.getAudioTracks()[0].id,
  1822. stream: event,
  1823. streamid: event.id,
  1824. hidden: false,
  1825. muted: false,
  1826. volume: 60,
  1827. scaleInfo: {},
  1828. };
  1829. const videoEl = createVideo({ appendChild: false, muted: true });
  1830. bodyAppendChildElArr.value.push(videoEl);
  1831. videoEl.setAttribute('videoid', microphoneVideoTrack.id);
  1832. videoEl.srcObject = event;
  1833. microphoneVideoTrack.videoEl = videoEl;
  1834. const res = [...appStore.allTrack, microphoneVideoTrack];
  1835. appStore.setAllTrack(res);
  1836. cacheStore.setResourceList(res);
  1837. handleMixedAudio();
  1838. console.log('获取麦克风成功');
  1839. } else if (val.type === MediaTypeEnum.txt) {
  1840. const txtTrack: AppRootState['allTrack'][0] = {
  1841. id: getRandomEnglishString(6),
  1842. openEye: true,
  1843. audio: 2,
  1844. video: 1,
  1845. mediaName: val.mediaName,
  1846. type: MediaTypeEnum.txt,
  1847. track: undefined,
  1848. trackid: undefined,
  1849. stream: undefined,
  1850. streamid: undefined,
  1851. hidden: false,
  1852. muted: false,
  1853. scaleInfo: {},
  1854. };
  1855. if (fabricCanvas.value) {
  1856. const canvasDom = markRaw(
  1857. new fabric.Text(val.txtInfo?.txt || '', {
  1858. top: 0,
  1859. left: 0,
  1860. fill: val.txtInfo?.color,
  1861. })
  1862. );
  1863. handleMoving({ canvasDom, id: txtTrack.id });
  1864. handleScaling({ canvasDom, id: txtTrack.id });
  1865. txtTrack.txtInfo = val.txtInfo;
  1866. if (window.devicePixelRatio !== 1) {
  1867. const ratio = 1 / window.devicePixelRatio;
  1868. canvasDom.scale(ratio);
  1869. txtTrack.scaleInfo[window.devicePixelRatio] = {
  1870. scaleX: ratio,
  1871. scaleY: ratio,
  1872. };
  1873. } else {
  1874. txtTrack.scaleInfo[window.devicePixelRatio] = { scaleX: 1, scaleY: 1 };
  1875. }
  1876. txtTrack.canvasDom = canvasDom;
  1877. fabricCanvas.value.add(canvasDom);
  1878. }
  1879. const res = [...appStore.allTrack, txtTrack];
  1880. // @ts-ignore
  1881. appStore.setAllTrack(res);
  1882. // @ts-ignore
  1883. cacheStore.setResourceList(res);
  1884. console.log('获取文字成功', fabricCanvas.value);
  1885. } else if (val.type === MediaTypeEnum.time) {
  1886. const timeTrack: AppRootState['allTrack'][0] = {
  1887. id: getRandomEnglishString(6),
  1888. openEye: true,
  1889. audio: 2,
  1890. video: 1,
  1891. mediaName: val.mediaName,
  1892. type: MediaTypeEnum.time,
  1893. track: undefined,
  1894. trackid: undefined,
  1895. stream: undefined,
  1896. streamid: undefined,
  1897. hidden: false,
  1898. muted: false,
  1899. scaleInfo: {},
  1900. };
  1901. if (fabricCanvas.value) {
  1902. const canvasDom = markRaw(
  1903. new fabric.Text(new Date().toLocaleString(), {
  1904. top: 0,
  1905. left: 0,
  1906. fill: val.timeInfo?.color,
  1907. })
  1908. );
  1909. setScaleInfo({ canvasDom, track: timeTrack });
  1910. timeCanvasDom.value.push(canvasDom);
  1911. handleMoving({ canvasDom, id: timeTrack.id });
  1912. handleScaling({ canvasDom, id: timeTrack.id });
  1913. timeTrack.timeInfo = val.timeInfo;
  1914. timeTrack.canvasDom = canvasDom;
  1915. fabricCanvas.value.add(canvasDom);
  1916. }
  1917. const res = [...appStore.allTrack, timeTrack];
  1918. // @ts-ignore
  1919. appStore.setAllTrack(res);
  1920. // @ts-ignore
  1921. cacheStore.setResourceList(res);
  1922. console.log('获取时间成功', fabricCanvas.value);
  1923. } else if (val.type === MediaTypeEnum.stopwatch) {
  1924. const stopwatchTrack: AppRootState['allTrack'][0] = {
  1925. id: getRandomEnglishString(6),
  1926. openEye: true,
  1927. audio: 2,
  1928. video: 1,
  1929. mediaName: val.mediaName,
  1930. type: MediaTypeEnum.stopwatch,
  1931. track: undefined,
  1932. trackid: undefined,
  1933. stream: undefined,
  1934. streamid: undefined,
  1935. hidden: false,
  1936. muted: false,
  1937. scaleInfo: {},
  1938. };
  1939. if (fabricCanvas.value) {
  1940. const canvasDom = markRaw(
  1941. new fabric.Text('00天00时00分00秒000毫秒', {
  1942. top: 0,
  1943. left: 0,
  1944. fill: val.stopwatchInfo?.color,
  1945. // editable: true,
  1946. })
  1947. );
  1948. setScaleInfo({ canvasDom, track: stopwatchTrack });
  1949. stopwatchCanvasDom.value.push(canvasDom);
  1950. handleMoving({ canvasDom, id: stopwatchTrack.id });
  1951. handleScaling({ canvasDom, id: stopwatchTrack.id });
  1952. stopwatchTrack.stopwatchInfo = val.stopwatchInfo;
  1953. stopwatchTrack.canvasDom = canvasDom;
  1954. fabricCanvas.value.add(canvasDom);
  1955. }
  1956. const res = [...appStore.allTrack, stopwatchTrack];
  1957. // @ts-ignore
  1958. appStore.setAllTrack(res);
  1959. // @ts-ignore
  1960. cacheStore.setResourceList(res);
  1961. console.log('获取秒表成功', fabricCanvas.value);
  1962. } else if (val.type === MediaTypeEnum.img) {
  1963. const imgTrack: AppRootState['allTrack'][0] = {
  1964. id: getRandomEnglishString(6),
  1965. openEye: true,
  1966. audio: 2,
  1967. video: 1,
  1968. mediaName: val.mediaName,
  1969. type: MediaTypeEnum.img,
  1970. track: undefined,
  1971. trackid: undefined,
  1972. stream: undefined,
  1973. streamid: undefined,
  1974. hidden: false,
  1975. muted: false,
  1976. scaleInfo: {},
  1977. };
  1978. if (fabricCanvas.value) {
  1979. if (!val.imgInfo) return;
  1980. const file = val.imgInfo[0].file!;
  1981. const { code } = await saveFile({ file, fileName: imgTrack.id });
  1982. if (code !== 1) return;
  1983. const imgEl = await new Promise<HTMLImageElement>((resolve) => {
  1984. const reader = new FileReader();
  1985. reader.addEventListener(
  1986. 'load',
  1987. function () {
  1988. const img = document.createElement('img');
  1989. img.src = reader.result as string;
  1990. img.onload = () => {
  1991. resolve(img);
  1992. };
  1993. },
  1994. false
  1995. );
  1996. if (file) {
  1997. reader.readAsDataURL(file);
  1998. }
  1999. });
  2000. const canvasDom = markRaw(
  2001. new fabric.Image(imgEl, {
  2002. top: 0,
  2003. left: 0,
  2004. width: imgEl.width,
  2005. height: imgEl.height,
  2006. })
  2007. );
  2008. const scale = handleScale({ width: imgEl.width, height: imgEl.height });
  2009. setScaleInfo({ canvasDom, track: imgTrack, scale });
  2010. handleMoving({ canvasDom, id: imgTrack.id });
  2011. handleScaling({ canvasDom, id: imgTrack.id });
  2012. imgTrack.canvasDom = canvasDom;
  2013. fabricCanvas.value.add(canvasDom);
  2014. }
  2015. const res = [...appStore.allTrack, imgTrack];
  2016. // @ts-ignore
  2017. appStore.setAllTrack(res);
  2018. // @ts-ignore
  2019. cacheStore.setResourceList(res);
  2020. console.log('获取图片成功', fabricCanvas.value);
  2021. } else if (val.type === MediaTypeEnum.media) {
  2022. const mediaVideoTrack: AppRootState['allTrack'][0] = {
  2023. id: getRandomEnglishString(6),
  2024. openEye: true,
  2025. audio: 2,
  2026. video: 1,
  2027. mediaName: val.mediaName,
  2028. type: MediaTypeEnum.media,
  2029. track: undefined,
  2030. trackid: undefined,
  2031. stream: undefined,
  2032. streamid: undefined,
  2033. hidden: false,
  2034. muted: false,
  2035. scaleInfo: {},
  2036. rect: { left: 0, top: 0 },
  2037. };
  2038. if (fabricCanvas.value) {
  2039. if (!val.mediaInfo) return;
  2040. const file = val.mediaInfo[0].file!;
  2041. const { code } = await saveFile({ file, fileName: mediaVideoTrack.id });
  2042. if (code !== 1) return;
  2043. const { videoEl, canvasDom, scale, stream } = await autoCreateVideo({
  2044. file,
  2045. id: mediaVideoTrack.id,
  2046. muted: mediaVideoTrack.muted,
  2047. rect: mediaVideoTrack.rect,
  2048. scaleInfo: mediaVideoTrack.scaleInfo,
  2049. });
  2050. setScaleInfo({ canvasDom, track: mediaVideoTrack, scale });
  2051. mediaVideoTrack.videoEl = videoEl;
  2052. mediaVideoTrack.canvasDom = canvasDom;
  2053. mediaVideoTrack.stream = stream;
  2054. mediaVideoTrack.streamid = stream.id;
  2055. mediaVideoTrack.track = stream.getVideoTracks()[0];
  2056. mediaVideoTrack.trackid = stream.getVideoTracks()[0].id;
  2057. if (stream.getAudioTracks()[0]) {
  2058. console.log('视频有音频', stream.getAudioTracks()[0]);
  2059. mediaVideoTrack.audio = 1;
  2060. mediaVideoTrack.volume = appStore.normalVolume;
  2061. const audioTrack: AppRootState['allTrack'][0] = {
  2062. id: mediaVideoTrack.id,
  2063. openEye: true,
  2064. audio: 1,
  2065. video: 2,
  2066. mediaName: val.mediaName,
  2067. type: MediaTypeEnum.media,
  2068. track: stream.getAudioTracks()[0],
  2069. trackid: stream.getAudioTracks()[0].id,
  2070. stream,
  2071. streamid: stream.id,
  2072. hidden: true,
  2073. muted: false,
  2074. volume: mediaVideoTrack.volume,
  2075. scaleInfo: {},
  2076. };
  2077. const res = [...appStore.allTrack, audioTrack];
  2078. appStore.setAllTrack(res);
  2079. cacheStore.setResourceList(res);
  2080. handleMixedAudio();
  2081. }
  2082. }
  2083. const res = [...appStore.allTrack, mediaVideoTrack];
  2084. // @ts-ignore
  2085. appStore.setAllTrack(res);
  2086. // @ts-ignore
  2087. cacheStore.setResourceList(res);
  2088. console.log('获取视频成功', fabricCanvas.value);
  2089. }
  2090. }
  2091. function editMediaOk(val: AppRootState['allTrack'][0]) {
  2092. showMediaModalCpt.value = false;
  2093. const res = appStore.allTrack.map((item) => {
  2094. if (item.id === val.id) {
  2095. item.mediaName = val.mediaName;
  2096. item.timeInfo = val.timeInfo;
  2097. item.stopwatchInfo = val.stopwatchInfo;
  2098. item.txtInfo = val.txtInfo;
  2099. if (
  2100. [
  2101. MediaTypeEnum.txt,
  2102. MediaTypeEnum.time,
  2103. MediaTypeEnum.stopwatch,
  2104. ].includes(val.type)
  2105. ) {
  2106. if (item.canvasDom) {
  2107. // @ts-ignore
  2108. item.canvasDom.set(
  2109. 'fill',
  2110. val.txtInfo?.color ||
  2111. val.timeInfo?.color ||
  2112. val.stopwatchInfo?.color
  2113. );
  2114. }
  2115. }
  2116. if (val.type === MediaTypeEnum.txt) {
  2117. if (item.canvasDom) {
  2118. // @ts-ignore
  2119. item.canvasDom.set('text', val.txtInfo?.txt);
  2120. }
  2121. }
  2122. if (val.openEye) {
  2123. if (item.canvasDom) {
  2124. item.canvasDom.opacity = 1;
  2125. }
  2126. } else {
  2127. if (item.canvasDom) {
  2128. item.canvasDom.opacity = 0;
  2129. }
  2130. }
  2131. }
  2132. return item;
  2133. });
  2134. appStore.setAllTrack(res);
  2135. cacheStore.setResourceList(res);
  2136. }
  2137. function handleChangeMuted(item: AppRootState['allTrack'][0]) {
  2138. if (item.videoEl) {
  2139. const res = !item.muted;
  2140. item.volume = res ? 0 : appStore.normalVolume;
  2141. item.muted = res;
  2142. if (item.type) {
  2143. if (item.type !== MediaTypeEnum.microphone) {
  2144. item.videoEl.muted = res;
  2145. }
  2146. item.videoEl.volume = res ? 0 : appStore.normalVolume / 100;
  2147. }
  2148. cacheStore.setResourceList(appStore.allTrack);
  2149. handleMixedAudio();
  2150. }
  2151. }
  2152. function handleChangeVolume(item: AppRootState['allTrack'][0], v) {
  2153. const res = appStore.allTrack.map((iten) => {
  2154. if (iten.id === item.id) {
  2155. if (item.volume !== undefined) {
  2156. iten.volume = v;
  2157. iten.muted = v === 0;
  2158. if (iten.videoEl && item.type) {
  2159. iten.videoEl.volume = v / 100;
  2160. if (item.type !== MediaTypeEnum.microphone) {
  2161. iten.videoEl.muted = v === 0;
  2162. }
  2163. }
  2164. }
  2165. }
  2166. return iten;
  2167. });
  2168. appStore.setAllTrack(res);
  2169. cacheStore.setResourceList(res);
  2170. handleMixedAudio();
  2171. }
  2172. function handleEye(item: AppRootState['allTrack'][0]) {
  2173. let current;
  2174. appStore.allTrack.forEach((iten) => {
  2175. if (iten.id === item.id) {
  2176. iten.openEye = !iten.openEye;
  2177. current = iten;
  2178. }
  2179. });
  2180. if (current) {
  2181. editMediaOk(current);
  2182. }
  2183. cacheStore.setResourceList(appStore.allTrack);
  2184. }
  2185. function handleActiveObject(item: AppRootState['allTrack'][0]) {
  2186. let current: AppRootState['allTrack'][0] | undefined;
  2187. appStore.allTrack.forEach((iten) => {
  2188. if (iten.id === item.id) {
  2189. current = iten;
  2190. }
  2191. });
  2192. if (current?.canvasDom) {
  2193. fabricCanvas.value?.setActiveObject(current.canvasDom);
  2194. }
  2195. }
  2196. function handleDel(item: AppRootState['allTrack'][0]) {
  2197. if (item.canvasDom !== undefined) {
  2198. fabricCanvas.value?.remove(item.canvasDom);
  2199. }
  2200. const delItem = appStore.allTrack.filter((iten) => iten.id === item.id);
  2201. delItem.forEach((v) => {
  2202. v.videoEl?.pause();
  2203. v.videoEl?.removeAttribute('src');
  2204. v.videoEl?.remove();
  2205. v.stream?.getTracks().forEach((track) => {
  2206. track.stop();
  2207. v.stream?.removeTrack(track);
  2208. });
  2209. });
  2210. bodyAppendChildElArr.value.forEach((el) => {
  2211. const videoid = el.getAttribute('videoid');
  2212. if (item.id === videoid) {
  2213. el.remove();
  2214. }
  2215. });
  2216. const res = appStore.allTrack.filter((iten) => iten.id !== item.id);
  2217. appStore.setAllTrack(res);
  2218. cacheStore.setResourceList(res);
  2219. handleMixedAudio();
  2220. }
  2221. function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
  2222. currentMediaType.value = item.type;
  2223. showMediaModalCpt.value = true;
  2224. }
  2225. </script>
  2226. <style lang="scss" scoped>
  2227. .slider {
  2228. width: 80px;
  2229. }
  2230. .push-wrap {
  2231. display: flex;
  2232. justify-content: space-between;
  2233. margin: 15px auto 0;
  2234. width: $w-1250;
  2235. .left {
  2236. position: relative;
  2237. display: inline-block;
  2238. box-sizing: border-box;
  2239. width: $w-960;
  2240. height: 100%;
  2241. border-radius: 6px;
  2242. background-color: white;
  2243. color: #9499a0;
  2244. vertical-align: top;
  2245. .container {
  2246. position: relative;
  2247. height: 100%;
  2248. background-color: rgba($color: #000000, $alpha: 0.5);
  2249. line-height: 0;
  2250. .recording {
  2251. position: absolute;
  2252. top: 5px;
  2253. left: 5px;
  2254. z-index: 100;
  2255. color: red;
  2256. font-size: 12px;
  2257. line-height: 1;
  2258. }
  2259. .record-ico {
  2260. position: absolute;
  2261. top: 0;
  2262. left: -10px;
  2263. cursor: pointer;
  2264. transform: translateX(-100%);
  2265. }
  2266. .add-wrap {
  2267. position: absolute;
  2268. top: 50%;
  2269. left: 50%;
  2270. display: flex;
  2271. align-items: center;
  2272. justify-content: space-around;
  2273. padding: 10px 20px;
  2274. width: 50%;
  2275. border-radius: 6px;
  2276. background-color: white;
  2277. transform: translate(-50%, -50%);
  2278. }
  2279. }
  2280. .room-control {
  2281. display: flex;
  2282. justify-content: space-between;
  2283. padding: 20px;
  2284. background-color: papayawhip;
  2285. .info {
  2286. display: flex;
  2287. align-items: center;
  2288. .avatar {
  2289. margin-right: 20px;
  2290. width: 55px;
  2291. height: 55px;
  2292. border-radius: 50%;
  2293. background-position: center center;
  2294. background-size: cover;
  2295. background-repeat: no-repeat;
  2296. }
  2297. .detail {
  2298. display: flex;
  2299. flex-direction: column;
  2300. flex-shrink: 0;
  2301. width: 200px;
  2302. text-align: initial;
  2303. .top {
  2304. margin-bottom: 10px;
  2305. color: #18191c;
  2306. }
  2307. .bottom {
  2308. font-size: 14px;
  2309. }
  2310. }
  2311. }
  2312. .rtc {
  2313. display: flex;
  2314. align-items: center;
  2315. flex: 1;
  2316. font-size: 14px;
  2317. .item {
  2318. display: flex;
  2319. align-items: center;
  2320. flex: 1;
  2321. .txt {
  2322. flex-shrink: 0;
  2323. width: 80px;
  2324. }
  2325. .down {
  2326. width: 90px;
  2327. user-select: none;
  2328. }
  2329. }
  2330. }
  2331. .other {
  2332. display: flex;
  2333. flex-direction: column;
  2334. justify-content: center;
  2335. font-size: 12px;
  2336. .top {
  2337. }
  2338. .bottom {
  2339. margin-top: 10px;
  2340. }
  2341. }
  2342. }
  2343. }
  2344. .right {
  2345. display: flex;
  2346. flex-direction: column;
  2347. justify-content: space-between;
  2348. box-sizing: border-box;
  2349. margin-left: 10px;
  2350. width: $w-300;
  2351. border-radius: 6px;
  2352. background-color: white;
  2353. color: #9499a0;
  2354. .resource-card {
  2355. position: relative;
  2356. box-sizing: border-box;
  2357. margin-bottom: 10px;
  2358. padding: 10px;
  2359. width: 100%;
  2360. height: 290px;
  2361. border-radius: 6px;
  2362. background-color: papayawhip;
  2363. .title {
  2364. text-align: initial;
  2365. }
  2366. .list {
  2367. overflow: scroll;
  2368. width: calc(100% + 5px);
  2369. height: 218px;
  2370. @extend %customScrollbar;
  2371. .item {
  2372. display: flex;
  2373. align-items: center;
  2374. justify-content: space-between;
  2375. margin: 5px 0;
  2376. font-size: 14px;
  2377. cursor: pointer;
  2378. user-select: none;
  2379. .item-left {
  2380. display: flex;
  2381. align-items: center;
  2382. height: 100%;
  2383. .control-item {
  2384. height: 100%;
  2385. line-height: 0;
  2386. cursor: pointer;
  2387. &:not(:last-child) {
  2388. margin-right: 6px;
  2389. }
  2390. }
  2391. }
  2392. .control {
  2393. display: flex;
  2394. align-items: center;
  2395. .control-item {
  2396. line-height: 0;
  2397. cursor: pointer;
  2398. &:not(:last-child) {
  2399. margin-right: 6px;
  2400. }
  2401. }
  2402. }
  2403. }
  2404. }
  2405. .bottom {
  2406. position: absolute;
  2407. bottom: 0;
  2408. left: 0;
  2409. padding: 10px;
  2410. }
  2411. }
  2412. .danmu-card {
  2413. position: relative;
  2414. flex: 1;
  2415. box-sizing: border-box;
  2416. padding: 10px 10px 0px;
  2417. width: 100%;
  2418. border-radius: 6px;
  2419. background-color: papayawhip;
  2420. text-align: initial;
  2421. .title {
  2422. margin-bottom: 10px;
  2423. }
  2424. .list {
  2425. overflow: scroll;
  2426. height: 266px;
  2427. @extend %customScrollbar;
  2428. .item {
  2429. box-sizing: border-box;
  2430. margin-bottom: 4px;
  2431. padding: 2px 0;
  2432. white-space: normal;
  2433. word-wrap: break-word;
  2434. font-size: 13px;
  2435. .name {
  2436. color: #9499a0;
  2437. cursor: pointer;
  2438. &.system {
  2439. color: red;
  2440. }
  2441. }
  2442. .msg {
  2443. margin-top: 4px;
  2444. color: #61666d;
  2445. &.img {
  2446. img {
  2447. width: 80%;
  2448. }
  2449. }
  2450. }
  2451. }
  2452. }
  2453. .send-msg {
  2454. position: relative;
  2455. box-sizing: border-box;
  2456. padding: 4px 0;
  2457. width: 100%;
  2458. .control {
  2459. display: flex;
  2460. margin: 4px 0;
  2461. .emoji-list {
  2462. position: absolute;
  2463. top: 0;
  2464. right: 0;
  2465. left: 0;
  2466. overflow: scroll;
  2467. box-sizing: border-box;
  2468. padding: 3px;
  2469. padding-right: 0;
  2470. height: 160px;
  2471. background-color: #fff;
  2472. transform: translateY(-100%);
  2473. @extend %customScrollbar;
  2474. .item {
  2475. display: inline-flex;
  2476. align-items: center;
  2477. justify-content: center;
  2478. box-sizing: border-box;
  2479. width: 14%;
  2480. height: 18%;
  2481. border: 1px solid #f8f8f8;
  2482. font-size: 20px;
  2483. cursor: pointer;
  2484. }
  2485. }
  2486. .ico {
  2487. margin-right: 6px;
  2488. width: 24px;
  2489. height: 24px;
  2490. cursor: pointer;
  2491. .input-upload {
  2492. width: 0;
  2493. height: 0;
  2494. opacity: 0;
  2495. }
  2496. &.face {
  2497. @include setBackground('@/assets/img/msg-face.webp');
  2498. }
  2499. &.img {
  2500. @include setBackground('@/assets/img/msg-img.webp');
  2501. }
  2502. }
  2503. }
  2504. .ipt {
  2505. display: block;
  2506. box-sizing: border-box;
  2507. margin: 0 auto;
  2508. padding: 10px;
  2509. width: 100%;
  2510. height: 60px;
  2511. outline: none;
  2512. border: 1px solid hsla(0, 0%, 60%, 0.2);
  2513. border-radius: 4px;
  2514. background-color: #f1f2f3;
  2515. font-size: 14px;
  2516. }
  2517. .btn {
  2518. box-sizing: border-box;
  2519. margin-top: 10px;
  2520. margin-left: auto;
  2521. padding: 4px;
  2522. width: 70px;
  2523. border-radius: 4px;
  2524. background-color: $theme-color-gold;
  2525. color: white;
  2526. text-align: center;
  2527. font-size: 12px;
  2528. cursor: pointer;
  2529. }
  2530. }
  2531. // .send-msg {
  2532. // display: flex;
  2533. // align-items: center;
  2534. // box-sizing: border-box;
  2535. // width: calc(100% - 20px);
  2536. // .ipt {
  2537. // display: block;
  2538. // box-sizing: border-box;
  2539. // margin: 0 auto;
  2540. // margin-right: 10px;
  2541. // padding: 10px;
  2542. // width: 80%;
  2543. // height: 30px;
  2544. // outline: none;
  2545. // border: 1px solid hsla(0, 0%, 60%, 0.2);
  2546. // border-radius: 4px;
  2547. // background-color: #f1f2f3;
  2548. // font-size: 14px;
  2549. // }
  2550. // }
  2551. }
  2552. }
  2553. }
  2554. // 屏幕宽度大于1500的时候
  2555. @media screen and (min-width: $w-1500) {
  2556. .push-wrap {
  2557. width: $w-1475;
  2558. .left {
  2559. width: $w-1150;
  2560. }
  2561. .right {
  2562. width: $w-300;
  2563. }
  2564. }
  2565. }
  2566. </style>