Silicon Valley Classroom 15_Live broadcast management module and WeChat sharing

Day 15 of Silicon Valley Classroom - Live Streaming Management Module and WeChat Sharing

1. Background system - live broadcast management module

1. Front-end integration of live course management

1.1, service_vod add method

(1) CourseController add method

@GetMapping("findAll")
public Result findAll() {
    List<Course> list = courseService.findlist();
    return Result.ok(list);
}

(2) CourseService implementation method

@Override
public List<Course> findlist() {
    List<Course> list = baseMapper.selectList(null);
    list.stream().forEach(item -> {
        this.getTeacherAndSubjectName(item);
    });
    return list;
}
1.2, router->index.js routing
{
  path: '/live',
  component: Layout,
  redirect: '/live/liveCourse/list',
  name: 'Live',
  meta: {
    title: 'Live management',
    icon: 'el-icon-bangzhu'
  },
  alwaysShow: true,
  children: [
    {
      path: 'liveCourse/list',
      name: 'liveCourseList',
      component: () => import('@/views/live/liveCourse/list'),
      meta: { title: 'Live List' }
    },
    {
      path: 'liveCourse/config/:id',
      name: 'liveCourseConfig',
      component: () => import('@/views/live/liveCourse/config'),
      meta: { title: 'Live configuration' },
      hidden: true
    },
    {
      path: 'liveVisitor/list/:id',
      name: 'liveVisitor',
      component: () => import('@/views/live/liveVisitor/list'),
      meta: { title: 'Watch History' },
      hidden: true
    }
  ]
},
1.3. Define the calling interface

import request from '@/utils/request'

const api_name = '/admin/live/liveCourse'

export default {

  getPageList(page, limit) {
    return request({
      url: `${api_name}/${page}/${limit}`,
      method: 'get'
    })
  },

  findLatelyList() {
    return request({
      url: `${api_name}/findLatelyList`,
      method: 'get'
    })
  },

  getById(id) {
    return request({
      url: `${api_name}/getInfo/${id}`,
      method: 'get'
    })
  },

  getLiveCourseAccount(id) {
    return request({
      url: `${api_name}/getLiveCourseAccount/${id}`,
      method: 'get'
    })
  },

  save(liveCourse) {
    return request({
      url: `${api_name}/save`,
      method: 'post',
      data: liveCourse
    })
  },

  updateById(liveCourse) {
    return request({
      url: `${api_name}/update`,
      method: 'put',
      data: liveCourse
    })
  },
  removeById(id) {
    return request({
      url: `${api_name}/remove/${id}`,
      method: 'delete'
    })
  },
  removeRows(idList) {
    return request({
      url: `${api_name}/batchRemove`,
      method: 'delete',
      data: idList
    })
  },

  getCourseConfig(id) {
    return request({
      url: `${api_name}/getCourseConfig/${id}`,
      method: 'get'
    })
  },

  updateConfig(liveCourseConfigVo) {
    return request({
      url: `${api_name}/updateConfig`,
      method: 'put',
      data: liveCourseConfigVo
    })
  },
}

findAll() {
  return request({
    url: `${api_name}/findAll`,
    method: 'get'
  })
},
1.4. Create a live page

(1)list.vue
<template>
  <div class="app-container">

    <!-- Toolbar -->
    <el-card class="operate-container" shadow="never">
      <i class="el-icon-tickets" style="margin-top: 5px"></i>
      <span style="margin-top: 5px">Datasheets</span>
      <el-button class="btn-add" size="mini" @click="add">Add to</el-button>
    </el-card>

    <!-- banner list -->
    <el-table
      v-loading="listLoading"
      :data="list"
      stripe
      border
      style="width: 100%;margin-top: 10px;">

      <el-table-column
        label="serial number"
        width="50"
        align="center">
        <template slot-scope="scope">
          {{ (page - 1) * limit + scope.$index + 1 }}
        </template>
      </el-table-column>
      <el-table-column label="cover" width="200" align="center">
        <template slot-scope="scope">
          <img :src="scope.row.cover" width="100%">
        </template>
      </el-table-column>
      <el-table-column prop="courseName" label="Live name" />
      <el-table-column prop="startTime" label="Live time">
        <template slot-scope="scope">
          {{ scope.row.param.startTimeString }}to{{ scope.row.param.endTimeString }}
        </template>
      </el-table-column>
      <el-table-column prop="endTime" label="Live end time" />
      <el-table-column prop="param.teacherName" label="Live teacher" />
      <el-table-column label="title" width="90">
        <template slot-scope="scope">
          <el-tag v-if="scope.row.param.teacherLevel === 1" type="success" size="mini">senior lecturer</el-tag>
          <el-tag v-if="scope.row.param.teacherLevel === 0" size="mini">Chief Lecturer</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="createTime" label="creation time" />

      <el-table-column label="operate" width="200" align="center">
        <template slot-scope="scope">
          <el-button type="text" size="mini" @click="edit(scope.row.id)">Revise</el-button>
          <el-button type="text" size="mini" @click="removeDataById(scope.row.id)">delete</el-button>
          <el-button type="text" size="mini" @click="showAccount(scope.row)">View account</el-button>
          <router-link :to="'/live/liveCourse/config/'+scope.row.id">
            <el-button type="text" size="mini">configure</el-button>
          </router-link>
          <router-link :to="'/live/liveVisitor/list/'+scope.row.id">
            <el-button type="text" size="mini">Watch History</el-button>
          </router-link>
        </template>
      </el-table-column>
    </el-table>

    <!-- pagination component -->
    <el-pagination
      :current-page="page"
      :total="total"
      :page-size="limit"
      :page-sizes="[5, 10, 20, 30, 40, 50, 100]"
      style="padding: 30px 0; text-align: center;"
      layout="sizes, prev, pager, next, jumper, ->, total, slot"
      @current-change="fetchData"
      @size-change="changeSize"
    />

    <el-dialog title="Add to/Revise" :visible.sync="dialogVisible" width="60%" >
      <el-form ref="flashPromotionForm" label-width="150px" size="small" style="padding-right: 40px;">
        <!-- course instructor -->
        <el-form-item label="Live lecturer">
          <el-select
            v-model="liveCourse.teacherId"
            placeholder="please choose">
            <el-option
              v-for="teacher in teacherList"
              :key="teacher.id"
              :label="teacher.name"
              :value="teacher.id"/>
          </el-select>
        </el-form-item>
        <el-form-item label="Live instructor login password" v-if="liveCourse.id === ''">
          <el-input v-model="liveCourse.password"/>
        </el-form-item>
        <el-form-item label="Live name">
          <el-input v-model="liveCourse.courseName"/>
        </el-form-item>
        <el-form-item label="Live start time">
          <el-date-picker
            v-model="liveCourse.startTime"
            type="datetime"
            placeholder="Choose a start date"
            value-format="yyyy-MM-dd HH:mm:ss" />
        </el-form-item>
        <el-form-item label="Live end time">
          <el-date-picker
            v-model="liveCourse.endTime"
            type="datetime"
            placeholder="Choose an end date"
            value-format="yyyy-MM-dd HH:mm:ss" />
        </el-form-item>
        <el-form-item label="Live Cover">
          <el-upload
            :show-file-list="false"
            :on-success="handleCoverSuccess"
            :before-upload="beforeCoverUpload"
            :on-error="handleCoverError"
            :action="BASE_API+'/admin/vod/file/upload?module=cover'"
            class="cover-uploader">
            <img v-if="liveCourse.cover" :src="liveCourse.cover" width="60%">
            <i v-else class="el-icon-plus avatar-uploader-icon"/>
          </el-upload>
        </el-form-item>
        <el-form-item label="Live Details">
          <el-input v-model="liveCourse.description" type="textarea" rows="5"/>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false" size="small">Cancel</el-button>
        <el-button type="primary" @click="saveOrUpdate()" size="small">Sure</el-button>
      </span>
    </el-dialog>

    <el-dialog title="View account" :visible.sync="accountDialogVisible" width="60%" >
      <el-form ref="accountForm" label-width="150px" size="small" style="padding-right: 40px;">
        <div style="margin-left: 40px;">
          <h4>Host help information</h4>
          <el-row style="height:35px;">
            <el-co >
              <span class="spd-info">Host login link:</span>
              <span class="spd-info">https://live.zhibodun.com/live/courseLogin.php?course_id={{ liveCourseAccount.courseId }}&role=admin</span>
            </el-co>
          </el-row>
          <el-row style="height:35px;">
            <el-col >
              <span class="spd-info">Host login password:{{ liveCourseAccount.zhuboKey }}</span>
            </el-col>
          </el-row>
        </div>
        <div style="margin-left: 40px;">
          <h4>Host client account information</h4>
          <el-row style="height:35px;">
            <el-col >
              <span class="spd-info">The anchor login account:{{ liveCourseAccount.zhuboAccount }}</span>
            </el-col>
          </el-row>
          <el-row style="height:35px;">
            <el-col >
              <span class="spd-info">Host login password:{{ liveCourseAccount.zhuboPassword }}</span>
            </el-col>
          </el-row>
        </div>

        <div style="margin-left: 40px;">
          <h4>Teaching Assistant Information</h4>
          <el-row style="height:35px;">
            <el-co >
              <span class="spd-info">Teaching assistant login link:</span>
              <span class="spd-info">https://live.zhibodun.com/live/courseLogin.php?course_id={{ liveCourseAccount.courseId }}&role=admin</span>
            </el-co>
          </el-row>
          <el-row style="height:35px;">
            <el-col>
              <span class="spd-info">Host login password:{{ liveCourseAccount.adminKey }}</span>
            </el-col>
          </el-row>
        </div>
        <div style="margin-left: 40px;">
          <h4>Student viewing information</h4>
          <el-row style="height:35px;">
            <el-co >
              <span class="spd-info">Watch the link:</span>
              <span class="spd-info">http://glkt-api.atguigu.cn/#/liveInfo/{{ liveCourseAccount.courseId }}</span>
            </el-co>
          </el-row>
          <el-row style="height:35px;">
            <el-col>
              <span class="spd-info">Watch the QR code:<img src="@/styles/qrcode.png" width="80px"/></span>
            </el-col>
          </el-row>
        </div>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="accountDialogVisible = false" size="small">closure</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
import api from '@/api/live/liveCourse'
import teacherApi from '@/api/vod/teacher'

const defaultForm = {
  id: '',
  courseName: '',
  startTime: '',
  endTime: '',
  teacherId: '',
  password: '',
  description: '',
  cover: 'https://cdn.uviewui.com/uview/swiper/1.jpg'
}
export default {
  data() {
    return {
      BASE_API: 'http://localhost:8333',
      listLoading: true, // Is the data loading
      list: null, // banner list
      total: 0, // The total number of records in the database
      page: 1, // Default page number
      limit: 10, // records per page
      searchObj: {}, // query form object

      teacherList: [], // Lecturer List

       dialogVisible: false,
       liveCourse: defaultForm,
       saveBtnDisabled: false,

      accountDialogVisible: false,
      liveCourseAccount: {
        courseId: ''
      }
    }
  },

  // Lifecycle function: the memory is ready, the page has not been rendered
  created() {
    console.log('list created......')
    this.fetchData()

    // Get a list of instructors
    this.initTeacherList()
  },

  // Life cycle function: memory is ready, page rendering is successful
  mounted() {
    console.log('list mounted......')
  },

  methods: {

    // When the page number changes
    changeSize(size) {
      console.log(size)
      this.limit = size
      this.fetchData(1)
    },

    // Load banner list data
    fetchData(page = 1) {
      console.log('Turn pages. . .' + page)
      // Fetch remote data asynchronously (ajax)
      this.page = page

      api.getPageList(this.page, this.limit).then(
        response => {
          this.list = response.data.records
          this.total = response.data.total

          // Data loaded and bound successfully
          this.listLoading = false
        }
      )
    },

    // Get a list of instructors
    initTeacherList() {
      teacherApi.list().then(response => {
        this.teacherList = response.data
      })
    },

    // reset query form
    resetData() {
      console.log('reset query form')
      this.searchObj = {}
      this.fetchData()
    },

    // delete data by id
    removeDataById(id) {
      // debugger
      this.$confirm('This action will permanently delete the record, Whether to continue?', 'hint', {
        confirmButtonText: 'Sure',
        cancelButtonText: 'Cancel',
        type: 'warning'
      }).then(() => { // promise
        // Click OK to call ajax remotely
        return api.removeById(id)
      }).then((response) => {
        this.fetchData(this.page)
        if (response.code) {
          this.$message({
            type: 'success',
            message: 'successfully deleted!'
          })
        }
      }).catch(() => {
        this.$message({
          type: 'info',
          message: 'Undeleted'
        })
      })
    },

    // -------------
    add(){
      this.dialogVisible = true
      this.liveCourse = Object.assign({}, defaultForm)
    },

    edit(id) {
      this.dialogVisible = true
      this.fetchDataById(id)
    },

    fetchDataById(id) {
      api.getById(id).then(response => {
        this.liveCourse = response.data
      })
    },

    saveOrUpdate() {
      this.saveBtnDisabled = true // Prevent form resubmission
      if (!this.liveCourse.id) {
        this.saveData()
      } else {
        this.updateData()
      }
    },

    // new
    saveData() {
      api.save(this.liveCourse).then(response => {
        if (response.code) {
          this.$message({
            type: 'success',
            message: response.message
          })
          this.dialogVisible = false
          this.fetchData(this.page)
        }
      })
    },

    // Update record based on id
    updateData() {
      api.updateById(this.liveCourse).then(response => {
        if (response.code) {
          this.$message({
            type: 'success',
            message: response.message
          })
          this.dialogVisible = false
          this.fetchData(this.page)
        }
      })
    },

    // Query records by id
    fetchDataById(id) {
      api.getById(id).then(response => {
        this.liveCourse = response.data
      })
    },

    showAccount(row) {
      this.accountDialogVisible = true
      api.getLiveCourseAccount(row.id).then(response => {
        this.liveCourseAccount = response.data
        this.liveCourseAccount.courseId = row.courseId
      })
    },

    // ------------upload------------
    // Upload success callback
    handleCoverSuccess(res, file) {
      this.liveCourse.cover = res.data
    },

    // Upload verification
    beforeCoverUpload(file) {
      const isJPG = file.type === 'image/jpeg'
      const isLt2M = file.size / 1024 / 1024 < 2

      if (!isJPG) {
        this.$message.error('Upload avatar images can only be JPG Format!')
      }
      if (!isLt2M) {
        this.$message.error('Upload avatar image size cannot exceed 2 MB!')
      }
      return isJPG && isLt2M
    },

    // error handling
    handleCoverError() {
      console.log('error')
      this.$message.error('Upload failed 2')
    },
  }
}
</script>
<style scoped>
  .cover-uploader .avatar-uploader-icon {
    border: 1px dashed #d9d9d9;
    border-radius: 6px;
    cursor: pointer;
    position: relative;
    overflow: hidden;

    font-size: 28px;
    color: #8c939d;
    width: 450px;
    height: 200px;
    line-height: 200px;
    text-align: center;
  }
  .cover-uploader .avatar-uploader-icon:hover {
    border-color: #409EFF;
  }
  .cover-uploader img {
    width: 450px;
    height: 200px;
    display: block;
  }
</style>
(2)config.vue
<template>
  <div class="app-container">
    <el-form label-width="120px" size="small">

      <div style="background-color:#E0E0E0;width: 100%;padding: 1px 10px;margin: 10px 0;"><h3>
        Function settings&nbsp;&nbsp;&nbsp;
      </h3></div>
      <el-form-item label="interface mode">
        <el-radio-group v-model="liveCourseConfigVo.pageViewMode">
          <el-radio :label="1">full screen mode</el-radio>
          <el-radio :label="0">Two split screen</el-radio>
          <el-radio :label="2">Courseware Mode</el-radio>
        </el-radio-group>
      </el-form-item>
      <el-form-item label="Viewer switch">
        <el-radio-group v-model="liveCourseConfigVo.numberEnable">
          <el-radio :label="1">Yes</el-radio>
          <el-radio :label="0">no</el-radio>
        </el-radio-group>
      </el-form-item>
      <el-form-item label="mall switch:">
        <el-radio-group v-model="liveCourseConfigVo.storeEnable">
          <el-radio :label="1">Yes</el-radio>
          <el-radio :label="0">no</el-radio>
        </el-radio-group>
      </el-form-item>

      <div style="background-color:#E0E0E0;width: 100%;padding: 1px 10px;margin: 10px 0;"><h3>
        Product list&nbsp;&nbsp;&nbsp;
        <el-button type="" size="mini" @click="addCourse()">Add to</el-button>
      </h3></div>
      <el-table
        v-loading="listLoading"
        :data="liveCourseConfigVo.liveCourseGoodsList"
        stripe
        border
        style="width: 100%;margin-top: 10px;">
        <el-table-column
          label="serial number"
          width="70"
          align="center">
          <template slot-scope="scope">
            {{ scope.$index + 1 }}
          </template>
        </el-table-column>
        <el-table-column label="product picture" width="120" align="center">
          <template slot-scope="scope">
            <img :src="scope.row.img" width="80px">
          </template>
        </el-table-column>
        <el-table-column prop="name" label="name" width="100"/>
        <el-table-column prop="price" label="price" width="100"/>
        <el-table-column prop="originalPrice" label="original price"/>
        <el-table-column label="operate" width="100" align="center">
          <template slot-scope="scope">
            <el-button type="text" size="mini" @click="removeCourseById(scope.$index)">delete</el-button>
          </template>
        </el-table-column>
      </el-table>

      <el-dialog title="Add a course" :visible.sync="dialogVisible" width="50%">
        <el-form :inline="true" label-width="150px" size="small" style="padding-right: 40px;">
          <el-table
            v-loading="listLoading"
            :data="courseList"
            stripe
            border
            style="width: 100%;margin-top: 10px;"
            @selection-change="handleSelectionChange">
            <el-table-column
              type="selection"
              width="55" />
            <el-table-column
              label="serial number"
              width="70"
              align="center">
              <template slot-scope="scope">
                {{ scope.$index + 1 }}
              </template>
            </el-table-column>
            <el-table-column label="Classification">
              <template slot-scope="scope">
                {{ scope.row.param.subjectParentTitle }} > {{ scope.row.param.subjectTitle }}
              </template>
            </el-table-column>
            <el-table-column prop="title" label="Course Title" width="150"/>
            <el-table-column prop="lessonNum" label="class" width="100"/>
            <el-table-column prop="param.teacherName" label="lecturer"/>
          </el-table>
          <el-form-item style="margin-top: 10px;">
            <el-button type="" @click="dialogVisible = false">Cancel</el-button>
            <el-button type="" @click="selectCourse()">save</el-button>
          </el-form-item>
        </el-form>
      </el-dialog>

      <br/><br/>
      <el-form-item>
        <el-button type="primary" @click="saveOrUpdate">save</el-button>
        <el-button @click="back">return</el-button>
      </el-form-item>
    </el-form>

  </div>
</template>

<script>
import api from '@/api/live/liveCourse'
import courseApi from '@/api/vod/course'

const defaultForm = {
  id: '',
  liveCourseId: '',
  pageViewMode: 1,
  numberEnable: 1,
  storeEnable: 1,
  storeType: 1,
  liveCourseGoodsList: []
}

export default {
  data() {
    return {
      listLoading: true, // Is the data loading

      liveCourseConfigVo: defaultForm,
      saveBtnDisabled: false,

      dialogVisible: false,
      courseList: [],
      multipleSelection: [] // List of records selected in bulk select
    }
  },

  // listener
  watch: {
    $route(to, from) {
      console.log('routing changes......')
      console.log(to)
      console.log(from)
      this.init()
    }
  },

  // Life cycle method (will not be called when the route is switched and the component remains unchanged)
  created() {
    console.log('form created ......')
    this.init()
  },

  methods: {

    // Form initialization
    init() {
      this.liveCourseConfigVo.liveCourseId = this.$route.params.id
      this.fetchDataById(this.liveCourseConfigVo.liveCourseId)

      this.fetchCourseList()
    },

    back() {
      this.$router.push({ path: '/live/liveCourse/list' })
    },

    // Query records by id
    fetchDataById(id) {
      api.getCourseConfig(id).then(response => {
        if(null !== response.data.id) {
          this.liveCourseConfigVo = response.data
        }
        this.listLoading = false
      })
    },

    fetchCourseList() {
      courseApi.findAll().then(response => {
        //debugger
        this.courseList = response.data
      })
    },

    handleSelectionChange(selection) {
      console.log(selection)
      this.multipleSelection = selection
    },

    addCourse() {
      this.dialogVisible = true
    },

    selectCourse() {
      if (this.multipleSelection.length === 0) {
        this.$message({
          type: 'warning',
          message: 'Please select the corresponding course!'
        })
        return
      }
      var list = []
      this.multipleSelection.forEach(item => {
        var obj = {
          liveCourseId: this.liveCourseConfigVo.liveCourseId,
          goodsId: item.id,
          name: item.title,
          img: item.cover,
          price: item.price,
          originalPrice: item.price,
          tab: '1',
          url: 'http://glkt-api.atguigu.cn/#/courseInfo/'+item.id,
          putaway: '1',
          pay: '1',
          qrcode: ''
        }
        list.push(obj)
      })
      this.liveCourseConfigVo.liveCourseGoodsList = list
      this.dialogVisible = false
    },

    removeCourseById(index) {
      this.liveCourseConfigVo.liveCourseGoodsList.splice(index, 1)
    },

    saveOrUpdate() {
      api.updateConfig(this.liveCourseConfigVo).then(response => {
        this.$message({
          type: 'success',
          message: response.message
        })
        this.$router.push({ path: '/live/liveCourse/list' })
      })
    }
  }
}
</script>
<style scoped>

  .littleMarginTop {
    margin-top: 10px;
  }

  .paramInput {
    width: 250px;
  }

  .paramInputLabel {
    display: inline-block;
    width: 100px;
    text-align: right;
    padding-right: 10px
  }

  .cardBg {
    background: #F8F9FC;
  }
</style>

2. Public account live broadcast connection

1. User viewing terminal integration

Interface documentation: https://open.talk-fun.com/docs/js/index.html

1.1. Obtain user access_token

To watch the live broadcast, the user must obtain the corresponding user access_token, and obtain the watched live course through the access_token;

Interface parameters: live broadcast id, user id

(1) Create LiveCourseApiController

@RestController
@RequestMapping("api/live/liveCourse")
public class LiveCourseApiController {

	@Resource
	private LiveCourseService liveCourseService;

    @ApiOperation(value = "get users access_token")
    @GetMapping("getPlayAuth/{id}")
    public Result<JSONObject> getPlayAuth(@PathVariable Long id) {
        JSONObject object = liveCourseService.getPlayAuth(id, AuthContextHolder.getUserId());
        return Result.ok(object);
    }

}

(2) Add method of LiveCourseService

JSONObject getPlayAuth(Long id, Long userId);

(3) LiveCourseServiceImpl implementation method

@SneakyThrows
@Override
public JSONObject getPlayAuth(Long id, Long userId) {
    LiveCourse liveCourse = this.getById(id);
    UserInfo userInfo = userInfoFeignClient.getById(userId);
    HashMap<Object,Object> options = new HashMap<Object, Object>();
    String res = mtCloudClient.courseAccess(liveCourse.getCourseId().toString(), userId.toString(), userInfo.getNickName(), MTCloud.ROLE_USER, 80*80*80, options);
    CommonResult<JSONObject> commonResult = JSON.parseObject(res, CommonResult.class);
    if(Integer.parseInt(commonResult.getCode()) == MTCloud.CODE_SUCCESS) {
        JSONObject object = commonResult.getData();
        System.out.println("access::"+object.getString("access_token"));
        return object;
    } else {
        throw new GgktException(20001,"Get failed");
    }
}
1.2. Download the front-end SDK

Download address: https://open.talk-fun.com/docs/js/download.html

1.3. Use shortcut templates

Download the template and modify the token acquisition method

var url  = window.location.search
var token = url.split("=")[1]
1.4. Combining with front-end projects

(1) Create a live broadcast page live.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
  <title>TalkFun Live QuickStart v2.2</title>
  <style type="text/css">
    * {
      margin: 0;
      padding: 0;
      list-style-type: none;
      font-family: "Microsoft YaHei", "STHeiti";
    }
    .flash-wran {
      display: none;
      position: absolute;
      top: 0;
      width: 100%;
      padding: 5px 0;
      text-align: center;
      background: #fff6a2;
      border: 1px solid #ffd913;
    }
    .flash-wran a {
      color: red;
    }
    .wrapper {
      /*display: flex;*/
      padding: 10px;
    }
    #cameraPlayer, 
    #pptPlayer {
      height: auto;
      flex: 1;
      text-align: center;
      font-size: 12px;
      overflow: hidden;
    }
    #pptPlayer {
      height: 300px;
      width: 100%;
    }
    #modPptPlayer,
    #modCameraPlayer {
      margin-top: 10px;
      border: 1px solid #c7c7c7;
    }
    .chat-wrap {
      padding: 5px;
      margin: 10px;
      border: 2px solid #cccccc;
      background: #f1f1f1
    }
    .mod-chat-list {
      margin: 20px 0;
      border: 1px solid #CCCCCC;
      min-height: 100px;
      font-size: 12px;
      background: #dedede;
      padding: 5px;
      max-height: 200px;
      overflow-y: scroll;
    }
    .mod-chat-list li {
      padding: 2px 0;
      margin-bottom: 5px;
      border-bottom: 1px dotted #CCCCCC;
    }
    input {
      display: inline-block;
      width: 200px;
      padding: 5px;
    }
    button {
      display: inline-block;
      padding: 5px;
      margin-left: 5px;
    }
    #toast {
      padding: 20px;
      position: fixed;
      z-index: 100;
      display: none;
      background: rgba(212, 28, 28, 0.8);
      left: 50%;
      top: 30%;
      border-radius: 50em;
      font-size: 14px;
      color: #FFFFFF;
      box-shadow: 0 0 6px 0px #bb2d2d;
    }
    #talkfun-video-wrap, #talkfun-camera-wrap {
      position: relative;
      background: #000;
    }
  </style>
  <!-- #SDK version -->
  <!-- #Get latest version ==> http://open.talk-fun.com/docs/js/changelog/live.html -->
  <script type="text/javascript" src="https://static-1.talk-fun.com/open/TalkFun_SDK_Pack/v6.0/TalkFunWebSDK-6.2-2.min.js"></script>
</head>
<body>
  <!-- #toast -->
  <div id="toast"></div>
  <!-- #wrap -->
  <div class="wrapper">
    <!-- #Artboard Player -->
    <div id="pptPlayer">
      <p id="loaddoc">player Loading...</p>
    </div>
    <!-- #camera mode -->
    <div id="cameraPlayer">
      <p id="loadcam">Camera Loading...</p>
    </div>
    <!-- #Desktop Sharing|Video in-stream mode -->
    <div id="videoPlayer">
      <p id="loadplayer">video player Loading...</p>
    </div>
  </div>
  <!-- #chat -->
  <div class="chat-wrap">
    <h4>chat module</h4>
    <ul id="chat-list" class="mod-chat-list"></ul>
    <label>
      <input id="chatVal" type="text" /><button id="chatSubmit">send chat</button>
    </label>
  </div>
  <script>
    // [First step] How to get access_token => http://open.talk-fun.com/docs/getstartV2/access_token.html
    // [Step 2] Monitor/call method according to Api documentation method JS Api => http://open.talk-fun.com/docs/js/sdk.js.getstart.html
    var url  = window.location.search
	var token = url.split("=")[1]
    // More configuration items => https://open.talk-fun.com/docs/js/sdk.js.getstart.html?h=%E9%85%8D%E7%BD%AE%E9%A1%B9
    var HT = new MT.SDK.main(token, {
      config: {
        techOrder: 'FLV' // Safari recommends setting to HLS
      }
    }, function (data) {
      console.warn('sdk Loading completed', data)
    })
    // Connection Status
    HT.on('connect', function () {
      console.log('TalkFun communication => connection succeeded...')
    })
    // Courseware Player
    HT.whiteboardPlayer('pptPlayer', 'docPlayer', function (player) {
      console.log('Courseware Player => Initialization successful')
      document.querySelector('#loadplayer').innerHTML = 'The artboard module is loaded'
    })
    // Video In-Stream | Desktop Sharing
    HT.videoPlayer('videoPlayer', 'modVideoplayer', function (player) {
      console.log('video player => Initialization successful')
      document.querySelector('#loadplayer').innerHTML = 'In-stream video loading completed'
    })
    // webcam player
    HT.camera('cameraPlayer', 'modCameraPlayer', function () {
      console.log('webcam player => Initialization successful')
      document.querySelector('#loadcam').innerHTML = 'Camera module loaded complete'
    })
    // receive chat
    var receivedChat = function (chat) {
      var tpl = chat.nickname + ': ' + chat.msg
      var chatItem = document.createElement('li')
      chatItem.innerHTML = tpl
      chatItem.className = 'chat-' + chat.xid
      document.querySelector('#chat-list').appendChild(chatItem)
    }
    // Receive chat messages
    HT.on('chat:send', function (chat) {
      receivedChat(chat)
    })
    // send chat message
    document.querySelector('#chatSubmit').addEventListener('click', function () {
      var chatIpt = document.querySelector('#chatVal')
      var chatValue = chatIpt.value
      HT.emit('chat:send', { msg: chatValue }, function (res) {
        // Sent successfully
        if (Number(res.code) === 0) {
          receivedChat(res.data)
          chatIpt.value = ''
        } 
        // Failed to send
        else {
          console.warn(res.msg)
        }
      })
    }, false)
    // Flash plugin exception
    HT.on('flash:load:error', function (obj) {
      if (!obj.flash) {
        document.querySelector('#flashTip').style.display = 'block'
      }
    })
    // Course error message
    HT.on('live:course:access:error', function (res) {
      console.error('error message ==>', res)
    })
    // Course error message
    HT.on('system:room:error', function (res) {
      var toast = document.querySelector('#toast')
      if (typeof res === 'string') {
        toast.innerHTML = res.msg
      }
      else if (res.msg) {
        toast.innerHTML = res.msg
      }
      toast.style.display = 'block'
      var _left = toast.clientWidth / 2
      toast.style.marginLeft = -_left + 'px'
    })
  </script>
</body>
</html>

Viewers can click to watch on the live broadcast details page, obtain the access_token through the interface, and then jump to the live broadcast viewing page with the access_token parameter. The key code is:

liveInfo.vue

play() {
  api.getPlayAuth(this.liveCourseId).then(response => {
    console.log(response.data);
    window.location = './live.html?token='+response.data.access_token;
    this.finished = true;
  });
},

http://localhost:8080/live.html is the access method for live viewing

1.5. Supplementary interface

(1) LiveCourseApiController class

@ApiOperation("according to ID Inquire about courses")
@GetMapping("getInfo/{courseId}")
public Result getInfo(
    @ApiParam(value = "course ID", required = true)
    @PathVariable Long courseId){
    Map<String, Object> map = liveCourseService.getInfoById(courseId);
    return Result.ok(map);
}

(2) Implementation of LiveCourseServiceImpl

@Override
public Map<String, Object> getInfoById(Long id) {
    LiveCourse liveCourse = this.getById(id);
    liveCourse.getParam().put("startTimeString", new DateTime(liveCourse.getStartTime()).toString("yyyy year MM moon dd HH:mm"));
    liveCourse.getParam().put("endTimeString", new DateTime(liveCourse.getEndTime()).toString("yyyy year MM moon dd HH:mm"));
    Teacher teacher = teacherFeignClient.get(liveCourse.getTeacherId());
    LiveCourseDescription liveCourseDescription = liveCourseDescriptionService.getByLiveCourseId(id);

    Map<String, Object> map = new HashMap<>();
    map.put("liveCourse", liveCourse);
    map.put("liveStatus", this.getLiveStatus(liveCourse));
    map.put("teacher", teacher);
    if(null != liveCourseDescription) {
        map.put("description", liveCourseDescription.getDescription());
    } else {
        map.put("description", "");
    }
    return map;
}

3. WeChat sharing

1. Achieving goals

1. Share on-demand course details page

2. WeChat sharing implementation method

Reference document: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html

2.1. Bind the domain name

First log in to the WeChat public platform, enter "Settings and Development", and fill in "JS Interface Security Domain Name" in "Function Settings" of "Official Account Settings".

Description: The local test sets the intranet penetration address.

2.2. Public account test account configuration

2.3. Import JS files
<script src="http://res.wx.qq.com/open/js/jweixin-1.4.0.js" type="text/javascript"></script>

Introduce the front-end project /public/index.html file

2.4, package and share js

Refer to the official documentation package interface

The pages we need to share include live broadcast detail pages, on-demand course detail pages, etc. Therefore, after encapsulating the sharing code, we can directly import and call it on the corresponding page.

Create a new src/util/wxShare.js file

/**
 * WeChat js-sdk
 * Reference document: https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
 */
const wxShare = {
    /**
     * [wxRegister WeChat Api initialization]
     * @param  {Function} callback [ready Callback]
     */
    wxRegister(data,option) { //data is WeChat configuration information, option is the shared configuration content
        
        wx.config({
            debug: true, // enable debug mode
            appId: data.appId, // Required, the unique ID of the official account
            timestamp: data.timestamp, // Required, the timestamp when the signature was generated
            nonceStr: data.nonceStr, // Required, a random string to generate the signature
            signature: data.signature,// Required, signature, see appendix 1
            jsApiList: [
                'onMenuShareAppMessage'
            ] // Required, a list of JS interfaces to be used, see Appendix 2 for a list of all JS interfaces
        });
        wx.ready(function(){

            wx.onMenuShareAppMessage({
                title: option.title, // share title
                desc: option.desc, // share description
                link: option.link, // share link
                imgUrl: option.imgUrl, // share icon
                success() {
                    // The callback function that is executed after the user successfully shares
                    //  option.success()
                    console.log('ok');
                },
                cancel() {
                    // The callback function that is executed after the user cancels the sharing
                    // option.error()
                    console.log('cancel');
                }
            });
        });

        wx.error(function(res){
          // If the verification of the config information fails, the error function will be executed. If the signature expires and the verification fails, the specific error information can be viewed in the debug mode of config, or in the returned res parameter. For SPA, the signature can be updated here.
          //alert('error:'+JSON.stringify(res));
        });
    }
}
export default wxShare
2.5. Server-side interface

Added ShareController class

Note: Wechat sharing needs to encrypt the current url. Since our url routes are all marked with "#", the server cannot receive them, so the word "guiguketan" is used instead of "#".

@RestController
@RequestMapping("/api/wechat/share")
@Slf4j
public class ShareController {

    @Autowired
    private WxMpService wxMpService;

    @GetMapping("/getSignature")
    public Result getSignature(@RequestParam("url") String url) throws WxErrorException {
        String currentUrl = url.replace("guiguketan", "#");
        WxJsapiSignature jsapiSignature = wxMpService.createJsapiSignature(currentUrl);

        WxJsapiSignatureVo wxJsapiSignatureVo = new WxJsapiSignatureVo();
        BeanUtils.copyProperties(jsapiSignature, wxJsapiSignatureVo);
        wxJsapiSignatureVo.setUserEedId(Base64Util.base64Encode(AuthContextHolder.getUserId()+""));
        return Result.ok(wxJsapiSignatureVo);
    }

}
2.6. Sharing of on-demand course details

Page: courseInfo.vue

(1) Introduce sharing

import wxShare from '@/utils/wxShare'

(2) Code implementation

key code

wxRegister() {
  //Note: The background encrypted url must be the same as the current page url
  let url = window.location.href.replace('#', 'guiguketan')
  shareApi.getSignature(url).then(response => {
    console.log(response.data);
    //record sharing users
    let link = '';
    if(window.location.href.indexOf('?') != -1) {
      link = window.location.href + '&recommend=' + response.data.userEedId;
    } else {
      link = window.location.href + '?recommend=' + response.data.userEedId;
    }
    let option = {
      'title': this.courseVo.title,
      'desc': this.description,
      'link': link,
      'imgUrl': this.courseVo.cover
    }
    wxShare.wxRegister(response.data, option);
  });
}
2.7. Test

(1) Use the mobile phone test, other end tests may have errors

Tags: Front-end Vue.js wechat

Posted by jaydeesmalls on Wed, 12 Oct 2022 10:58:08 +0300