-
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 module
The "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
end
params
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
end
For 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,
},
}
end
This 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
end
This 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' },
},
},
}
end
This 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,
},
}
end
This 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
end
This 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
end
This 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
end
When 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
end
When 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
end
This 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.