- 
                Notifications
    
You must be signed in to change notification settings  - Fork 94
 
Writing Network Modules
Network modules are just Lua tables with some predefined methods.
There's two kinds of modules - oauth, and non-oauth.
An OAuth module will expose a method to create the service's OAuth registration URL, and a method for creating an account after the user's hit "Authorize" on the external service.
A non-OAuth module exposes a method to create a simple registration form, the module can request and save any pieces of data it needs. The main app will handle saving all the pieces of data.
There's two skeleton network modules to serve as a starting point,
networks/skeleton.oauth.lua and networks/skeleton.plain.lua
All modules expose methods for:
- Creating a metadata-editing form (multistreamer handles saving the data).
 - Updating any needed metadata and returning an RTMP URL when the user starts pushing video.
 
All modules also have a displayname field.
OAuth modules will have a redirect_uri field set by Multistreamer.
Most modules will begin with something like:
local config = require('lapis.config').get()
local encode_base64 = require('lapis.util.encoding').encode_base64
local decode_base64 = require('lapis.util.encoding').decode_base64
local encode_with_secret = require('lapis.util.encoding').encode_with_secret
local decode_with_secret = require('lapis.util.encoding').decode_with_secret
local Account = require'models.account'
local resty_sha1 = require'resty.sha1'
local str = require 'resty.string'
local http = require'resty.http'
local module = {}
module.displayname = 'Demo Module'
-- methods...
return moduleThe "display name" to be shown in the web UI.
If a network module should allow accounts to be shared with other users, this should be true. Right now, Facebook is the only account that doesn't allow sharing.
user is a User object
You'll need to make use of the redirect_uri field. Additionally, OAuth
services have some method of including a piece of data, you'll need to
include the user.id field in order to look up the user later.
function module.get_oauth_url(user)
  local encoded_user = encode_base64(encode_with_secret({ id = user.id }))
  return 'https://some/service?redirect_uri=' .. module.redirect_uri ..
         '&state=' .. encoded_user
endparams will be the query parameters from the OAuth app.
The module will then register one Account with the Account
model. The network module is responsible for checking that the Account
is unique before saving. All accounts have a hex-encoded sha1 network_user_id
field.
Individual Account objects have a keystore for saving bits of data, such as OAuth Access Tokens, and anything else you may need.
function M.register_oauth(params)
  local user, err = decode_with_secret(decode_base64(params.state))
  local httpc = http.new()
  -- make some requests, get the user id from your service
  local sha1 = resty_sha1:new()
  sha1:update(some-external-user-id)
  local network_user_id = str.to_hex(sha1:final())
  local some_user_name = -- somehow get a username to display
  local account = Account:find({
    network = module.name,
    network_user_id = network_user_id,
  })
  if not account then
    account = Account:create({
      user_id = user.id,
      network = module.name,
      network_user_id = network_user_id,
      name = some_user_name
    })
  else
    -- account already exists!
    -- check if this account belongs to the
    -- user, etc
  end
  account:set('some-field','something')
  return account , nil
endFor non-oauth modules, this will return a table describing a form for adding a new account, or for editing an existing account.
This form is also used for verifying required data has been entered.
The documentation for module.metadata_form (further down) has more details
on what the returned table should look like.
function module.create_form()
  return {
    [1] = {
      type = 'text',
      label = 'Name',
      key = 'name',
      required = true,
    },
    [2] = {
      type = 'text',
      label = 'URL',
      key = 'url',
      required = true,
    },
  }
endThis is the function for saving an account.
- 
user- the authenticated user object - 
account- account if this is an existing account, nil otherwise - 
params- the params from the account creation form 
The module is responsible for generating some type of unique identifier
for the account, when account is nil. If account is defined, the
module can just update the account.
function module.save_account(user,account,params)
  local account = account
  local err
  local sha1 = resty_sha1:new()
  sha1:update(params.something-that-should-be-unique)
  local key = str.to_hex(sha1:final())
  if not account then
    -- double-check that an account isn't being duplicated
    account, err = Account:find({
      network = M.network,
      network_user_id = key,
    })
  end
  if not account then
    -- looks like this is a new account
    account, err = Account:create({
      network = M.name,
      network_user_id = key,
      name = params.name,
      user_id = user.id
    })
    if not account then
        return false,err
    end
  else
    -- either account was already provided, or found with Account:find
    account:update({
      name = params.name,
    })
  end
  account:set('something',params.something-you-care-about)
  return account, nil
endThis function returns a table describing a form for per-stream settings.
The account and stream parameters are keystores for the user's account
and stream, respectively. The account keystore should be
used for retrieving account-wide settings (OAuth access tokens, etc),
while the stream keystore is for per-stream settings, like the stream title.
Each entry in the table needs a type, label,key field. You can
include a value field to include a pre-filled value.
When the user submits the form, Multistreamer will save each of the
keys to the stream keystore.
Supported field types:
- 
text- generate a regular, single-line text field. - 
textarea- generates a multiline text area - 
select- generate a dropdown 
The select field type requires another field - options, which
should be a table of objects, like:
options = { { value = 1, label = 'first' }, { value = 2, label = 'second' } }
function module.metadata_form(account, stream)
  local oauth_token = account:get('oauth-token')
  -- make some http requests, do some things, etc
  return {
    [1] = {
        type = 'text',
        label = 'Title',
        key = 'title',
        value = stream:get('title'),
    },
    [2] = {
        type = 'text',
        label = 'Game',
        key = 'game',
        value = stream:get('title'),
    },
    [3] = {
        type = 'select',
        label = 'Mature',
        key = 'mature',
        value = stream:get('mature'),
        options = {
            { value = 0, label = 'No' },
            { value = 1, label = 'Yes' },
        },
    },
  }
endThis function should return a table describing the required and optional
per-stream settings. This is used by multistreamer to actually save the
keys from the metadata_form function.
If the module doesn't require/accept any per-stream settings, just return something false-y.
All that's needed for each table entry is a key field and an optional
required field. All other fields are ignored.
function module.metadata_fields()
  return {
    [1] = {
        key = 'title',
        required = true,
    },
    [2] = {
        key = 'game',
        required = true,
    },
    [3] = {
        key = 'hashtags',
        required = false,
    },
  }
endThis function is called when the user starts streaming data. account
and stream are keystores to account-wide settings and per-stream
settings.
The returned function should take whatever actions it needs to create a video, update channel metadata, and get or generate an RTMP URL.
If available, it should also save a shareable HTTP URL to the stream keystore
as http_url - this URL is used for sending out notifications/tweets/etc.
Some example code
function module.publish_start(account,stream)
  local title = stream:get('title')
  -- do something with title, like make an http request to the service
  local rtmp_url = make_rtmp_url_somehow()
  return rtmp_url, nil
endThis function is called every 30 seconds while a stream is active.
If you need to make some type of request after streaming has started, this is where you'd do it. For example, the YouTube module has to 'transition' a live broadcast to 'live' after video has started, so it's done within this module.
If streaming needs to be stopped for some reason, return something false-y.
function module.notify_update(account, stream)
  local stream_id = stream:get('stream_id')
  -- do some things
  return true, nil
endThis function is called when the user stops streaming data. account
and stream are keystores to account-wide settings and per-stream
settings.
function module.publish_stop(account,stream)
  stream:unset('something')
  return
endWhen a stream goes live, this function is called to create read and write functions stream comments.
account and stream are per-account and per-stream tables (they're keystores
in table form), send is a function that should be called for every new
comment/chat/etc on the stream.
The returned read_func will be spawned into an nginx thread - it should
loop indefinitely. Whenever it has a new comment, it should call send with
a table like:
type = 'text', -- can be 'text' or 'emote'
text = 'message', -- only used with type='text'
markdown = 'markdown-message,' -- markdown variant of message (optional)
from = { name = 'name', id = 'id' }
The write_func will be called as-needed to post comments/chat messages
to the video stream. It should accept a single parameter - a table
with the keys type and text - type can be text (for posting a
standard comment/message) or emote for performing some kind of emote
action.
Example:
function module.create_comment_funcs(account,stream,send)
  local http_client = create_http_client(account['access_token'])
  local stream_id = stream['stream_id']
  local read_func = function()
    while true do
      local res, err = http_client:get_comments(stream_id)
      if res then
        for k,v in pairs(res) do
          send({
            type = 'text',
            text = v.message,
            markdown = v.markdown,
            from = {
              name = v.from.name,
              id = v.from.id,
            },
          })
        end
      end
      ngx.sleep(10)
    end
  end
  local write_func = function(msg)
    if msg.type == 'text' then
      http_client:post_comment(stream_id,msg.text)
    elseif mgs.type == 'emote' them
      http_client:post_emote(stream_id,msg.text)
    end
  end
  return read_func, write_func
endWhen a stream goes live, this function is called to create a function for getting the stream's viewer count.
account and stream are per-account and per-stream tables (they're keystores
in table form), send is a function that should be called whenever the
view count has updated.
The returned view_func will be spawned into an nginx thread - it should
loop indefinitely. Whenever it has an updated viewer count, it should call send with
a table like:
viewer_count = 10 -- the current viewer count
Example:
function module.create_viewcount_func(account,stream,send)
  local http_client = create_http_client(account['access_token'])
  local stream_id = stream['stream_id']
  local viewcount_func = function()
    while true do
      local res, err = http_client:get_viewcount(stream_id)
      if res then
        send({ viewer_count = res.viewcount })
      end
      ngx.sleep(10)
    end
  end
  return viewcount_func
endThis function is called everytime the user pulls up the main index page. It provides an opportunity to check if any keys are out-of-date, refresh metadata, etc.
It should either return some error text to display next to the account, or something false-y/nil if there's nothing to display.