live.service.ts 17 KB

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