index.vue 75 KB

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