live.service.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. import { Injectable } from '@angular/core';
  2. import { AlertController } from '@ionic/angular';
  3. import * as Parse from 'parse';
  4. import { AiChatService } from './aichart.service';
  5. import { HttpService } from './http.service';
  6. import { MessageService } from './message.service';
  7. declare const AgoraRTC: any;
  8. @Injectable({
  9. providedIn: 'root',
  10. })
  11. export class LiveService {
  12. isAnchor: boolean = false; //是否是主播
  13. options: {
  14. appid: string;
  15. channel: string;
  16. token: string;
  17. } = {
  18. appid: '',
  19. channel: '',
  20. token: '',
  21. };
  22. localTracks: any = {
  23. audioTrack: null,
  24. videoTrack: null,
  25. };
  26. rid?: string; //房间id(channel)
  27. profile?: any = localStorage.getItem('profile');
  28. client: any; //客户端
  29. company: string = '';
  30. UID: any;
  31. tools: any = {
  32. audio: false, //是否关闭音频
  33. camera: false, //是否切换摄像头
  34. mute: false, //是否静音
  35. };
  36. user_published_list = new Set(); //已发布列表
  37. timer: any; //轮询获取频道token的定时
  38. alert: any; //提示框
  39. media_devices: {
  40. //音视频设备列表
  41. audioDevices: Array<any>;
  42. videoDevices: Array<any>;
  43. } = {
  44. audioDevices: [],
  45. videoDevices: [],
  46. };
  47. currentUsedDevice: { audioDevice: string; videoDevice: string } = {
  48. //当前使用的设备
  49. audioDevice: '',
  50. videoDevice: '',
  51. };
  52. room?: Parse.Object; //直播间
  53. connection_state?: string; //连接状态
  54. surplusNumber: number = 0; //剩余可通话时长(单位:秒s)
  55. countdown: number = 0; // 新增倒计时变量
  56. timer_countdown: any;
  57. liveLog?: Parse.Object; //直播记录
  58. constructor(
  59. private http: HttpService,
  60. private aiServ: AiChatService,
  61. private alertController: AlertController,
  62. private msgSer: MessageService
  63. ) {
  64. this.client?.leave();
  65. this.company = this.aiServ.company;
  66. this.getProfile();
  67. }
  68. async getProfile() {
  69. if (this.profile) return;
  70. let queryProfile = new Parse.Query('Profile');
  71. queryProfile.equalTo('user', Parse.User.current()?.id);
  72. queryProfile.notEqualTo('isDeleted', true);
  73. queryProfile.equalTo('isCross', true);
  74. this.profile = await queryProfile.first();
  75. this.profile?.id &&
  76. localStorage.setItem('profile', JSON.stringify(this.profile.toJSON()));
  77. }
  78. /* 初始化Agora */
  79. initAgora() {
  80. this.timer && clearTimeout(this.timer);
  81. this.timer_countdown && clearInterval(this.timer_countdown);
  82. this.options['token'] = '';
  83. this.options['channel'] = '';
  84. this.client?.leave();
  85. this.client = AgoraRTC.createClient({ mode: 'rtc', codec: 'h264' });
  86. AgoraRTC.enableLogUpload();
  87. }
  88. // 获取所有音视频设备
  89. getDevices() {
  90. AgoraRTC.getDevices()
  91. .then((devices: any) => {
  92. this.media_devices.audioDevices = devices.filter(function (
  93. device: any
  94. ) {
  95. return device.kind === 'audioinput';
  96. });
  97. this.media_devices.videoDevices = devices.filter(function (
  98. device: any
  99. ) {
  100. return device.kind === 'videoinput';
  101. });
  102. console.log(this.media_devices);
  103. this.currentUsedDevice = {
  104. audioDevice: this.media_devices.audioDevices[0].deviceId,
  105. videoDevice: this.media_devices.videoDevices[0].deviceId,
  106. };
  107. // return Promise.all([
  108. // AgoraRTC.createCameraVideoTrack(),
  109. // AgoraRTC.createMicrophoneAudioTrack(),
  110. // ]);
  111. })
  112. .then((tracks: any) => {
  113. console.log(tracks);
  114. })
  115. .catch((err: any) => {
  116. console.warn('获取媒体权限失败', err);
  117. this.alertTips('获取媒体权限失败');
  118. });
  119. }
  120. /* 获取token */
  121. async getToken(room: Parse.Object) {
  122. this.room = room;
  123. this.isAnchor = room?.get('user')?.id === Parse.User.current()?.id;
  124. this.timer && clearTimeout(this.timer);
  125. let remoteEle = document.getElementById('vice-video');
  126. (remoteEle as any).style.display = 'none';
  127. //获取频道token记录
  128. let baseurl = 'https://server.fmode.cn/api/ailiao/token';
  129. if (this.isAnchor) {
  130. this.UID = 111111;
  131. baseurl = 'https://server.fmode.cn/api/webrtc/build_token';
  132. }
  133. let reqBody = {
  134. company: this.company, // this.aiSer.company,
  135. profile: room?.get('profile').id,
  136. channelName: room?.get('profile').id,
  137. };
  138. let data: any = await this.http.httpRequst(baseurl, reqBody, 'POST');
  139. console.log(data);
  140. if (data.code == 200) {
  141. this.options.token = data.data.token;
  142. this.options.appid = data.data.appid;
  143. } else {
  144. this.timer = setTimeout(() => {
  145. this.getToken(room);
  146. }, 2000);
  147. return;
  148. }
  149. this.options.channel = room?.get('profile').id;
  150. this.join();
  151. }
  152. async updateToken(pid: string) {
  153. let sql = `select "rtc"."objectId" as "rid","rtc"."channel", "rtc"."token", "rtc"."expiraTime" from "RtcToken" as "rtc" where "rtc"."profile" = '${pid}' and "rtc"."expiraTime" >= now() order by "createdAt" desc limit 1`;
  154. let tokenData: any = await this.http.customSQL(sql);
  155. if (
  156. tokenData &&
  157. tokenData.code == 200 &&
  158. tokenData.data &&
  159. tokenData.data.length > 0
  160. ) {
  161. return tokenData.data[0];
  162. } else {
  163. return null;
  164. }
  165. }
  166. /* 进入频道 */
  167. async join() {
  168. let path = location.pathname;
  169. if (path.indexOf('/live/link-room/') == -1) return;
  170. console.log('频道:', this.options.channel);
  171. this.client
  172. .join(
  173. this.options.appid,
  174. this.options.channel,
  175. this.options.token,
  176. this.UID
  177. )
  178. .then(async (uid: any) => {
  179. // await this.client.setClientRole('host');
  180. this.connection_state = this.client.connectionState;
  181. console.log('进入频道当前uid:', uid, '状态:', this.connection_state);
  182. let user = Parse.User.current();
  183. this.msgSer?.publishMessage(
  184. `${user?.get('name') || user?.get('nickname')}加入频道直播`,
  185. this.room?.get('user')?.id
  186. );
  187. if (!this.isAnchor) {
  188. // 观众进入直播间,创建直播记录
  189. await this.createLiveLog(uid);
  190. } else {
  191. //主播进入直播间直接发送自己推流
  192. await this.publishSelf();
  193. }
  194. await this.joinReady();
  195. this.afterJoin();
  196. })
  197. .catch((err: any) => {
  198. console.log('进入频道失败:', err);
  199. });
  200. }
  201. /* 发布本地视频 */
  202. async publishSelf() {
  203. try {
  204. let data = await Promise.all([
  205. /* 创建音频和视频轨道 */
  206. AgoraRTC.createMicrophoneAudioTrack(this.currentUsedDevice.audioDevice),
  207. AgoraRTC.createCameraVideoTrack(this.currentUsedDevice.videoDevice),
  208. ]);
  209. this.localTracks.audioTrack = data[0];
  210. this.localTracks.videoTrack = data[1];
  211. // console.log(this.localTracks);
  212. let remoteEle = document.getElementById('vice-video');
  213. if (remoteEle) {
  214. remoteEle.textContent = '';
  215. remoteEle.style.display = 'block';
  216. }
  217. this.localTracks.videoTrack.play('vice-video'); //播放自己视频渲染
  218. if (this.tools['audio']) {
  219. this.localTracks.audioTrack.setEnabled(false);
  220. } else {
  221. this.localTracks.audioTrack.setEnabled(true);
  222. }
  223. await this.client.publish(Object.values(this.localTracks));
  224. } catch (err) {
  225. console.log('发布本地视频失败:', err);
  226. this.alertTips('发布本地视频失败', '提示', () => history.back());
  227. }
  228. }
  229. /* 订阅远程视频 */
  230. async joinReady() {
  231. this.client.remoteUsers.forEach((user: any) => {
  232. console.log('remoteUsers', user.uid);
  233. this.client.subscribe(user, 'audio').then((audioTrack: any) => {
  234. this.user_published_list.add(user);
  235. audioTrack.setVolume(100);
  236. audioTrack.play();
  237. });
  238. this.client.subscribe(user, 'video').then((videoTrack: any) => {
  239. let remoteEle = document.getElementById('video');
  240. if (remoteEle) {
  241. remoteEle.textContent = '';
  242. }
  243. videoTrack.play('video');
  244. });
  245. });
  246. this.client.on('user-joined', (user: any) => {
  247. console.log(user, `${user.uid} 加入频道`);
  248. });
  249. this.client.on('user-published', async (user: any, mediaType: string) => {
  250. console.log('用户推流成功', user);
  251. await this.client.subscribe(user, mediaType);
  252. let remoteEle = document.getElementById('video');
  253. if (remoteEle) {
  254. remoteEle.textContent = '';
  255. }
  256. if (mediaType === 'video') {
  257. user.videoTrack.play('video');
  258. this.alert?.dismiss();
  259. }
  260. if (mediaType === 'audio') {
  261. user.audioTrack.play();
  262. this.user_published_list.add(user);
  263. }
  264. });
  265. this.client.on('user-unpublished', async (user: any, mediaType: string) => {
  266. if (mediaType === 'audio') {
  267. console.log('对方已静音');
  268. this.user_published_list.delete(user);
  269. }
  270. if (mediaType === 'video') {
  271. console.log('用户取消推流');
  272. let remoteEle = document.getElementById('video');
  273. if (remoteEle) {
  274. remoteEle.textContent = '对方离开直播间';
  275. }
  276. //主播离开,停止计时计费
  277. // if (!this.isAnchor) {
  278. this.client.leave();
  279. // }
  280. this.alertTips('对方已离开直播间');
  281. }
  282. });
  283. this.client.on(
  284. 'connection-state-change',
  285. (curState: any, prevState: any) => {
  286. this.connection_state = curState;
  287. if (
  288. curState == 'RECONNECTING' ||
  289. curState == 'DISCONNECTED' ||
  290. curState == 'DISCONNECTING'
  291. ) {
  292. this.timer && clearTimeout(this.timer);
  293. this.timer_countdown && clearInterval(this.timer_countdown);
  294. if (!this.isAnchor) {
  295. //用户离开直播间,断开与主播状态连接频道
  296. this.msgSer.unsubscribeMessage(this.room?.get('user')?.id);
  297. }
  298. }
  299. console.log('live状态变更:', this.connection_state);
  300. }
  301. );
  302. this.monitorDevices();
  303. }
  304. async createLiveLog(uid: string) {
  305. let profile = JSON.parse(localStorage.getItem('profile') || '{}');
  306. let obj = Parse.Object.extend('LiveLog');
  307. let liveLog = new obj();
  308. liveLog.set('title', `与${profile?.name || profile?.mobile}进行直播`);
  309. liveLog.set('uid', String(uid));
  310. liveLog.set('company', {
  311. __type: 'Pointer',
  312. className: 'Company',
  313. objectId: this.company,
  314. });
  315. liveLog.set('room', {
  316. __type: 'Pointer',
  317. className: 'Room',
  318. objectId: this.room?.id,
  319. });
  320. liveLog.set('user', {
  321. __type: 'Pointer',
  322. className: '_User',
  323. objectId: Parse.User.current()?.id,
  324. });
  325. liveLog.set('profile', {
  326. __type: 'Pointer',
  327. className: 'Profile',
  328. objectId: this.room?.get('profile')?.id,
  329. });
  330. this.liveLog = await liveLog.save();
  331. }
  332. /* 连线成功立即创建直播记录,同步获取剩余可通话时长,并且开始计时 */
  333. async afterJoin() {
  334. this.timer && clearTimeout(this.timer);
  335. this.timer_countdown && clearInterval(this.timer_countdown);
  336. const targetUser = this.client.remoteUsers?.find((user: any) => user.uid !== 100001) //排出超管
  337. console.log(targetUser);
  338. if (this.client.remoteUsers.length > 0 && targetUser) {
  339. if (this.isAnchor) {
  340. //如果是主播,进入获取remoteUsers.user获取livelog再获取对方剩余通话时长
  341. this.getLiveLog(targetUser.uid);
  342. } else {
  343. let query = new Parse.Query('LiveLog');
  344. query.equalTo('objectId', this.liveLog?.id);
  345. query.equalTo('isLive', true);
  346. query.select('objectId');
  347. const resultData = await query.first();
  348. if (resultData?.id) {
  349. //首次进入至少计时2分钟,不足2分钟按2分钟计算扣时
  350. await this.publishSelf();
  351. await this.get_duration();
  352. this.computeDuration(60000 * 2);
  353. } else {
  354. this.timer = setTimeout(() => {
  355. this.afterJoin();
  356. }, 1000);
  357. }
  358. }
  359. } else {
  360. this.timer = setTimeout(() => {
  361. this.afterJoin();
  362. }, 1000);
  363. }
  364. }
  365. async getLiveLog(tarUid:string) {
  366. // let uid = this.client.remoteUsers[0].uid;
  367. this.timer && clearTimeout(this.timer);
  368. let query = new Parse.Query('LiveLog');
  369. query.equalTo('uid', String(tarUid));
  370. query.equalTo('room', this.room?.id);
  371. query.notEqualTo('isDeleted', true);
  372. query.notEqualTo('isLive', true);
  373. query.descending('createdAt');
  374. this.liveLog = await query.first();
  375. if (this.liveLog?.id) {
  376. this.liveLog?.set('isLive', true);
  377. await this.liveLog?.save();
  378. this.get_duration();
  379. return;
  380. }
  381. this.timer = setTimeout(() => {
  382. this.getLiveLog(tarUid);
  383. }, 1000);
  384. }
  385. async get_duration() {
  386. this.timer_countdown && clearInterval(this.timer_countdown);
  387. let url = 'https://server.fmode.cn/api/ailiao/remain_second';
  388. let params = {
  389. rid: this.room?.id,
  390. uid: this.liveLog?.get('user')?.id || this.liveLog?.get('user')?.objectId,
  391. };
  392. let data = await this.http.httpRequst(url, params, 'POST');
  393. console.log(data);
  394. this.surplusNumber = data.data ?? 0;
  395. this.countdown = this.surplusNumber; // 初始化倒计时
  396. if (this.countdown <= 120) {
  397. this.alertTips('剩余通话时间不足2分钟,请及时充值');
  398. }
  399. let arr = ['RECONNECTING', 'DISCONNECTED', 'DISCONNECTING'];
  400. if (!arr.includes(this.connection_state as any)) {
  401. this.startCountdown();
  402. }
  403. }
  404. startCountdown() {
  405. this.timer_countdown = setInterval(() => {
  406. if (this.countdown > 0) {
  407. this.countdown--;
  408. // console.log(this.countdown);
  409. } else {
  410. clearInterval(this.timer_countdown);
  411. this.alertTips('通话时间结束');
  412. this.client.leave(); // 结束通话
  413. }
  414. }, 1000);
  415. }
  416. /* 开始计时 */
  417. async computeDuration(num?: number) {
  418. //每10秒保存一次数据
  419. this.timer && clearTimeout(this.timer);
  420. let p = JSON.parse(this.profile);
  421. console.log(p);
  422. let url = 'https://server.fmode.cn/api/ailiao/count/duration';
  423. let params = {
  424. company: this.company,
  425. profile: p?.objectId,
  426. rid: this.room?.id,
  427. uid: Parse.User.current()?.id,
  428. duration: num ? num / 1000 : 10,
  429. logid: this.liveLog?.id,
  430. };
  431. let data = await this.http.httpRequst(url, params, 'POST');
  432. console.log(data);
  433. this.timer = setTimeout(() => {
  434. this.computeDuration();
  435. }, num ?? 10000);
  436. }
  437. /* 监听音视频设备插拔 */
  438. monitorDevices() {
  439. AgoraRTC.onMicrophoneChanged = async (changedDevice: any) => {
  440. // 插入麦克风设备时,切换到新插入的设备
  441. if (changedDevice.state === 'ACTIVE') {
  442. this.localTracks.audioTrack.setDevice(changedDevice.device.deviceId);
  443. this.currentUsedDevice.audioDevice = changedDevice.device.deviceId;
  444. // 拔出设备为当前设备时,切换到一个已有的设备
  445. } else if (
  446. changedDevice.device.label ===
  447. this.localTracks.audioTrack.getTrackLabel()
  448. ) {
  449. const oldMicrophones = await AgoraRTC.getMicrophones();
  450. oldMicrophones[0] &&
  451. this.localTracks.audioTrack.setDevice(oldMicrophones[0].deviceId);
  452. this.currentUsedDevice.audioDevice = oldMicrophones[0].deviceId;
  453. }
  454. };
  455. AgoraRTC.onCameraChanged = async (changedDevice: any) => {
  456. // 插入相机设备时,切换到新插入的设备
  457. if (changedDevice.state === 'ACTIVE') {
  458. this.localTracks.videoTrack.setDevice(changedDevice.device.deviceId);
  459. this.currentUsedDevice.videoDevice = changedDevice.device.deviceId;
  460. // 拔出设备为当前设备时,切换到一个已有的设备
  461. } else if (
  462. changedDevice.device.label ===
  463. this.localTracks.videoTrack.getTrackLabel()
  464. ) {
  465. const oldCameras = await AgoraRTC.getCameras();
  466. oldCameras[0] &&
  467. this.localTracks.videoTrack.setDevice(oldCameras[0].deviceId);
  468. this.currentUsedDevice.videoDevice = oldCameras[0].deviceId;
  469. }
  470. };
  471. }
  472. /* 关开麦 */
  473. async updatePublishedAudioTrack() {
  474. this.tools['audio'] = !this.tools['audio'];
  475. if (this.tools['audio'] && this.localTracks.audioTrack) {
  476. await this.localTracks.audioTrack.setEnabled(false);
  477. console.log('停止推送音频');
  478. } else {
  479. await this.localTracks.audioTrack.setEnabled(true);
  480. console.log('恢复推送音频');
  481. }
  482. // await this.client.unpublish(this.localTracks.audioTrack);
  483. return true;
  484. }
  485. /* 静音 */
  486. async muteAudio() {
  487. this.tools['mute'] = !this.tools['mute'];
  488. let list = Array.from(this.user_published_list);
  489. for (let index = 0; index < list.length; index++) {
  490. const user = list[index];
  491. if (this.tools['mute']) {
  492. console.log('静音');
  493. await this.client.unsubscribe(user, 'audio');
  494. } else {
  495. await this.client.subscribe(user, 'audio');
  496. console.log('恢复声音');
  497. }
  498. }
  499. }
  500. /* 切换摄像头 */
  501. async changeCamera() {
  502. const oldCameras = await AgoraRTC.getCameras(true);
  503. let newCamers = oldCameras.find(
  504. (item: any) => item.deviceId !== this.currentUsedDevice.videoDevice
  505. );
  506. console.log(newCamers);
  507. newCamers &&
  508. this.localTracks.videoTrack
  509. .setDevice(newCamers.deviceId)
  510. .then(() => {
  511. this.tools['camera'] = !this.tools['camera'];
  512. this.currentUsedDevice.videoDevice = newCamers.deviceId;
  513. })
  514. .catch((err: any) => {
  515. console.log('set device error', err);
  516. this.alertTips('切换摄像头失败');
  517. });
  518. return true;
  519. }
  520. /* 提示 */
  521. async alertTips(message: string, title?: string, callBack?: Function) {
  522. this.alert = await this.alertController.create({
  523. header: title || '提示',
  524. message: message,
  525. backdropDismiss: false,
  526. buttons: [
  527. {
  528. text: '确认',
  529. handler: () => {
  530. callBack && callBack();
  531. },
  532. },
  533. ],
  534. });
  535. await this.alert.present();
  536. }
  537. }