# Kaltura migration MediaCMS can copy a Kaltura portal into itself: the media files, the people who own them, the galleries, channels and course spaces they sit in, and the captions. You start it, watch it, pause it when you want the machine back, and run it again later to pick up whatever was added since or whatever failed. Only superusers can see or use it. The entry point is **Migrations** in the top right menu. ## What comes over * Video, audio and image entries, with the original file and optionally the transcoded versions Kaltura already made. * Captions, converted to WebVTT. * Owners. An account is created for whoever owns an entry, or an existing account is reused when one matches. * Galleries and channels, as flat MediaCMS categories, each keeping its Kaltura id. Course channels from an LMS integration come over the same way, titled by their course name rather than by the LMS course id. See [Sites and learning platforms](#sites-and-learning-platforms). * Play counts, and whether the item is public, unlisted or private. * Category members and their permission level, when RBAC is turned on. See [Permissions](#permissions). * Playlists, in order, when **Migrate playlists** is on. See [Playlists](#playlists). Skipped: Kaltura's plain text transcripts, which have no MediaCMS equivalent, dynamic playlists, and live entries or anything else with no downloadable file. ## Getting the API credentials MediaCMS signs in with a Kaltura **app token**, so you never hand it the administrator secret for the account. A token is a separate credential, it carries only the privileges you gave it, and you can revoke it on its own. Two of the four values in the form are easy to find: * **Service URL**, the host the API lives on: `https://api.example.edu` for a self hosted or managed Kaltura, `https://www.kaltura.com` for SaaS. Leave off the `/api_v3` part. * **Partner ID**, the number in the KMC under Settings, then Integration Settings. The other two, **App token ID** and **App token value**, come from creating the token. ### Creating the app token There is no screen for this in the KMC, so use the API console at [developer.kaltura.com/console](https://developer.kaltura.com/console) or two curl calls. Start a session with your administrator secret. It is needed only for the call that follows and is never stored, so give it five minutes and let it die: ```bash curl -X POST 'https://api.example.edu/api_v3/service/session/action/start?format=1' \ -d 'secret=YOUR_ADMIN_SECRET' \ -d 'partnerId=YOUR_PARTNER_ID' \ -d 'type=2' \ -d 'expiry=300' ``` Use the session string it returns to create the token: ```bash curl -X POST 'https://api.example.edu/api_v3/service/appToken/action/add?format=1' \ -d 'ks=THE_SESSION_STRING_FROM_ABOVE' \ -d 'appToken:objectType=KalturaAppToken' \ -d 'appToken:sessionType=2' \ -d 'appToken:hashType=SHA256' \ -d 'appToken:sessionDuration=86400' \ -d 'appToken:expiry=2147483647' \ -d 'appToken:description=MediaCMS migration' ``` Three values matter: * **`sessionType=2`** gives the token **admin rights**, without which the migration sees only what one ordinary user can see. * **`sessionDuration`** is how long each session made from the token lasts, in seconds. A day is plenty: MediaCMS starts a fresh session for every batch of media, and silently renews one that expires mid call. * **`expiry`** is a date rather than a duration, and it is when the token itself stops working. Migrating a large portal can run for weeks, and this is the only one of these numbers that can end a run early, hence 2038 rather than something that merely sounds sensible. Some installations cap it or expire tokens by policy, so read the token back with `appToken.get` and check what Kaltura actually stored. The response holds an `id` and a `token`: the **id** goes in App token ID, the **token** in App token value. Keep the token safe, it is the half that acts as a password. MediaCMS encrypts it before storing it and never sends it back to the browser. When the migration is done, `appToken.delete` retires it and nothing else about the account changes. A token created with a different `hashType` is fine. MediaCMS works out which algorithm it uses on the first session, trying SHA256, then SHA1, SHA512 and MD5. ## Running a migration 1. Go to `/migrations`, press **New migration** and pick Kaltura. 2. Fill in the service URL, partner ID, app token ID and app token value. 3. Press **Test connection**. It reports how many entries, users and categories the account holds, loads the top level categories to choose from, and imports nothing. 4. Tick the categories to migrate, on the same tab. Test connection has just loaded them, and at least one is required. 5. Set the import options. 6. **Save draft**, or **Save and start** to begin. Nothing is imported until you start it. 7. Watch the migration page: progress, a live log, and the ID mapping table with links to both the Kaltura entry and the MediaCMS media. Credentials are encrypted at rest with a key derived from `SECRET_KEY`, and the API never returns a secret. The form shows a mask and keeps the stored value if you save without retyping it. Test connection makes the server talk to whatever address is in the form, which is one reason the feature is restricted to superusers. ## Import options Every migration is different, so read these before starting rather than after. | Option | Default | Effect | | --- | --- | --- | | Migrate all users | **chosen** | Every real account in the portal gets a MediaCMS account, whether or not it owns anything. | | Only migrate below listed users | | Fetch only the media owned by the Kaltura user ids typed in the same box, and create those owners. | | Only migrate users that own media | | Accounts are created as the media that belongs to them arrives. | | Do not run between | off | On the Migration scheduling tab. Holds the run inside a window of hours and resumes it by itself. See [Quiet hours](#quiet-hours). | | Run unpaused during weekends | off | On the Migration scheduling tab. The window above is ignored on Saturday and Sunday, portal timezone. | | LTI platform for LMS courses | none | Links migrated LMS courses to a platform set up under **MediaCMS Admin → LTI 1.3 Integration → LTI Platforms**, so they stay in step with the LMS. Needs `USE_LTI`. See [Sites and learning platforms](#sites-and-learning-platforms). | | Add entries in sub-categories to parent categories | on | A KMS category shows the content of its sub-categories; MediaCMS categories are flat, so this adds it. See [Flat categories](#flat-categories). | | Migrate playlists | off | Static playlists holding media in the chosen top level categories, or in no category, become MediaCMS playlists, in order, fetching any such media they name and creating its owners, whatever the user options say. Dynamic playlists are skipped. See [Playlists](#playlists). | | Migrate groups | off | Kaltura groups that are members of a category in the chosen top level categories become MediaCMS RBAC groups, and their members are created whatever the user options say. Their media is not fetched: that follows the user options. Needs `USE_RBAC`. See [Groups](#groups). | | Import captions | on | Caption assets become WebVTT subtitles. One that fails to convert is recorded as failed and the media keeps the rest. Plain text transcripts have no equivalent and are only logged. | | Import statistics (Plays/Views) | on | Copy play counts onto the migrated media. | | Maintain publish state of media | on | Derive public, unlisted or private from the source. Unmoderated media stays private. See [Permissions](#permissions). | | Migrate media flavors / encodings | on | Import existing Kaltura flavors as MediaCMS encodings instead of re-encoding. Off imports the original file only and MediaCMS encodes it. | | Categories | **required** | On the Connection tab, loaded by Test connection. Fetch the media in the chosen top level categories and everything beneath them, plus media in no category. Media filed only under other top level categories is left out. At least one is required. | ### How users are matched Accounts are created lazily, as the media that belong to them arrive, and an existing account is reused rather than duplicated: 1. **By its own earlier record.** A Kaltura user an earlier run already migrated keeps that account. 2. **By email**, case insensitively, unless another Kaltura user already holds that account. Kaltura lets several accounts share an email (a native account and the one an LMS created, typically), and merging them would pool their media and drop the second one's role. So the second one gets an account of its own, under its own username, with a unique address on the reserved `email-conflict.invalid` domain, and the run log names the conflict. Searching users for `email-conflict.invalid` after the run lists every account whose email an administrator still has to decide. 3. **By username**, but only when the existing account has no email of its own. Otherwise it is a different, already identified person who happens to collide on the sanitised username, and linking would hand them someone else's media. 4. **Otherwise a new account**, with an unusable password, a numeric suffix if the username is taken, and no notification email to anyone. The three user options are one question with three answers — sweep the whole directory, name the users to fetch, or create an owner as their media arrives — so choosing one clears the others and one is always chosen. **There is no fallback owner to set.** A media whose owner cannot be resolved, because the Kaltura account is gone or importing it failed, still needs somewhere to go: it goes to whoever set the migration up, and to an administrator if that account no longer exists. `fallback_username` is not a Kaltura option at all any more; a migration saved while it was one has the value dropped on its next save. With **Migrate all users** off, only people who own media get an account. The exception is the members of a private category, who are created regardless: see [Permissions](#permissions). Roles are covered under [Roles](#roles). ### Migrating only some categories Every migration is scoped to what you choose. There is no "migrate everything" switch, because there is no sensible case for pointing a migration at an entire Kaltura account and hoping, and a migration nobody has scoped would sweep the lot by accident. **Each top level category is its own site**, a KMS or KAF instance with its own users, and one Kaltura partner often holds several. A media filed only under top level categories you did not pick belongs to another site, so it is **not migrated**, and neither is its owner unless **Migrate all users** is chosen, which still brings every account on the partner. A media that sits both in a tree you picked and in one you did not comes over, and loses only the filing outside your selection. **A media in no category at all is migrated**, whoever owns it: nothing on it says which site it came from. Within that, the user options decide as before: every user's media by default, or only the listed users' media when you name them. The picker also decides **which categories exist**, and which of them a media may be filed under. The picker lives on the **Connection** tab, because what you can choose depends on the connection working first. Press **Test connection**: when it succeeds it loads the list, and you tick what you want. **Reload categories** fetches it again if the source has changed since. Only **top level** categories are offered. A top level category is a whole site, and ticking one brings every category and every media beneath it, so listing the levels below would ask the same question again in more places and would be unusable on a portal with thousands of channels. **At least one has to be ticked.** Save draft and Save and start are both refused until then, in the form and again on the server. An entry that belongs to no category arrives **private**, since the source published it nowhere. On the portal this was built against that is 17 of 74 entries, so expect a number of them. **Test connection** reports what the run would bring: users, media, categories, channels and groups. Captions are not counted there. Kaltura refuses to list caption assets without an entry id, so a caption total means fetching every entry id first and batching through them, which is a hundred extra calls before a large run has started. They are counted as the run discovers them instead, which is why the progress page shows captions without a denominator. Existing migrations saved before this carry a retired `migrate_all_categories` option, which is dropped the next time they are saved. The count shown is for the whole subtree. Kaltura's own `entriesCount` on a root counts direct members only, and a root has none, so it would report zero for a site holding thousands. The roots themselves do not become MediaCMS categories. A root with a `>site>` child is the handle for a KMS site or an LMS integration, often named something like `ltigeneric_8yrU6`, and turning it into a category would just wrap everything in a box named after the plumbing. A top level category on an ordinary portal has no `>site>` child and is kept, because there it is real content. ### Playlists A Kaltura playlist carries its members as an ordered list of entry ids, which is exactly the shape MediaCMS wants, so the order survives the trip. Playlists are imported **after** media, since one can only point at media the run has already brought over. Two kinds are left out. A **dynamic** playlist is a saved query rather than a list, and has no MediaCMS equivalent: snapshotting one would leave a playlist called "Most recent videos" frozen on whatever today happened to be. And KMS keeps a playlist per channel under an internal account, which is plumbing nobody would recognise. On the portal this was built against, that leaves 4 of 10 playlists worth migrating. **Only playlists of the chosen sites come over.** Playlists belong to the partner, not to a site, so one is migrated when at least one entry it names sits under a chosen top level category or in no category at all. A playlist made of another site's media is left out, and so is its owner. **A playlist can name media the run never fetched**, because the user options narrowed it or because the playlist crosses into someone else's content. Those are fetched on the playlist's account and their owners created: asking for playlists is asking for whole playlists, and a list with holes in it is worse than an import that reached a little wider than asked. The one exception is an entry filed only under top level categories you did not choose: it belongs to another site, so it is left out of the playlist and the log names it. Be aware of what that means. Switching playlists on fetches media owned by people outside **Only migrate below listed users**, and creates those people as users, or gives their media to the account the run falls back to. It is the one option that deliberately reaches past the others. Media pulled in this way is treated exactly like any other: the same state rules, and the same rule that it may only be filed under categories inside the chosen roots. An item whose categories all sit outside them gets none, and media in no category arrives **private**. So a playlist can legitimately contain items only their owner can see. Finally, playlists outlive their contents. A Kaltura playlist may name entries the source no longer holds at all, and nothing can rescue those: they are skipped and listed in the migration log, leaving a shorter playlist. The log says which of the two reasons applied. ### Groups and service accounts A Kaltura "user" is not always a person. Two kinds are filtered out of the user sweep. **Groups.** A group is a user with `type` GROUP, and without the filter it arrives as an account named after the group that nobody can log into. The sweep now asks for `typeIn=0`, so only people come through. Checked against a live portal, since Kaltura ignores a filter field it does not support: 28 users unfiltered, 27 with `typeIn=0`, 1 with `typeIn=1`. Switch **Migrate groups** on and those groups become MediaCMS RBAC groups instead, keeping who is in them. Groups belong to the partner, not to a site, so only a group that is a member of a category under a chosen top level category comes over; one used only by another site, or by no category at all, is left out with its members. Kaltura's group roles map straight across, MANAGER to manager and MEMBER to member. It needs `USE_RBAC`; with RBAC off each group is recorded as skipped with the reason, rather than disappearing quietly. Worth knowing before switching it on: **a group grants nothing on its own.** An RBAC group gives access to the categories attached to it, and a Kaltura group carries no categories, so what arrives is a membership list waiting to be pointed at something. That is still worth having, since rebuilding one by hand is the tedious work a migration is for, but it is not access control until somebody attaches it to a category. Where a group *is* a member of a category, the migration expands it into the people it stands for, each taking the permission level the group was given. Importing it as a person would otherwise create an account named after the group. **Service accounts.** Kaltura and KMS create accounts for their own plumbing: `__kms_logo_upload_user__`, `__kmsInternalChannelPlaylistUser__`, `__kms_config_upload_user__`, `kmsAdminServiceUser`, `kmsSaaSAdmin_*`, `Guest`, and a user literally called `0`. Sweeping every user turns each one into a MediaCMS account. They are matched on the id, by the double underscore wrapping Kaltura uses for its own accounts plus a short list of fixed KMS names, and left out of the sweep. **They are only left out of the sweep.** A service account that owns media still gets an account when that media arrives, so nothing is orphaned. On the portal this was checked against, `__kms_logo_upload_user__` owns one entry and keeps it. ### Sites and learning platforms A portal usually holds more than one tree: the MediaSpace site, and one per LMS integration that has been wired up. They behave differently and the migration works out which is which on its own, from `privacyContexts`. That field is Kaltura's own switch for this. Privacy and entitlement settings only take effect inside a privacy context. KMS stamps one on its site, the LTI integrations create their categories without one, and the field is inherited, so it reads the same on a course channel as on the root it hangs from. | | KMS site | LMS tree | | --- | --- | --- | | `privacyContexts` | set, e.g. `MediaSpace` | empty | | Category permissions | honoured, see [Permissions](#permissions) | none to honour | | Media state | from the category type | private, reached through the course | The consequence worth understanding: **the permission fields on an LMS category do nothing at the source**, because there is no privacy context for Kaltura to enforce them in. What gates LMS content is the course itself: it is shown to the people enrolled in it. So the migration reads LMS categories as private and imports no permissions from them; an entry only in LMS courses arrives private, and the course's RBAC group (see below) is how its people reach it. MediaCMS shows such an entry as **Shared**. An entry that is also in a public KMS gallery stays public. A payload with no `privacyContexts` field at all is read as KMS. An LMS integration also does not put a course's media in the course channel. It creates a child named **InContext** and parks the media there, leaving the course itself empty. That child is plumbing nobody browses, so no category is made for it and its media is rolled up to the course, which is the category a person recognises. Only in an LMS tree: a KMS gallery that somebody named InContext is a real category and is kept. **Wiring a course to an LTI platform.** With a platform chosen, a migrated course becomes an RBAC category with a group and an `LTIResourceLink`, so a launch from the LMS finds that category instead of building a second one beside it. Two conditions, both silent when unmet: * **Only a course is wired.** Being inside an LMS tree is not enough: the tree root, the `site`/`channels` scaffolding and any shared channel somebody added are all in that tree and none of them is a course. A course is the one carrying a `CourseName`, which is the same key the title comes from, so a category with no readable name is never wired. * **`USE_RBAC` must be on**, as well as `USE_LTI`. The group is the point of the exercise, so with RBAC off the course is imported as an ordinary category and the log says so. Nothing ever unwires a category. One wired by an earlier run keeps its platform, so correcting a mistake means clearing it by hand. **Course names.** An LTI integration names the course channel after the LMS course id, so the category is called `14` and nothing on the category says otherwise. The readable name is in the custom metadata Kaltura keeps beside it, under a `CourseName` key, and that is what the migrated category is titled. Without the metadata plugin, or without that key, the id stays as the title and the import carries on. Read per category, for LMS trees only. A course already imported under its id keeps that title; rename it on the category edit page rather than importing it twice. ### Category identity A migrated category takes the **Kaltura category id as its `uid`**, so the id in a MediaCMS category URL is the one you can paste back into Kaltura. That id is also how a category is recognised on a later run: one already carrying it is this same category and is reused rather than duplicated, even if the migration's own mapping row has been deleted. The `uid` column is unique across the portal, and two Kaltura installations number their categories from the same pool, so an id already held by a category with a different title is taken to be something else, a second portal or a category made by hand. That one keeps its `uid` and the import takes a suffixed one rather than failing. Under the hood this is `categoryAncestorIdIn`, not a membership filter. That distinction is the whole feature, and it is worth knowing that it was checked against a live portal rather than taken from the documentation: on ours an instance root reported 53 entries while holding none directly, which is both proof that descendants are included and proof that Kaltura is honouring the filter at all. It answers a filter field it does not support by ignoring it and returning everything, so a filter that silently did nothing would look like a filter that matched the whole portal. ### Migrating only specific users This narrows the whole run to the media owned by the ids you list, which is how to try a migration on a small known set first. The value has to be the user id **Kaltura stores on the entry**, which is not always an email address. A portal can hold a mix of `jdoe@example.edu` and opaque ids like `f777c82f755df2d4bc9688d340b69292`, whose account may have a completely different email, so look the id up in the KMC rather than guessing. Kaltura ignores a filter it cannot use, and answers an unknown user id with zero entries rather than an error, so both would fail quietly. Against that: an empty list is refused when saving and no filter is ever sent empty, **Test connection** reports the count per id so a mistyped one shows as its own `0`, and a run where every listed id has no media ends as an error rather than an empty success. Everything else follows the media that pass the filter, and the discovered total is the filtered count. Widen the list later, or switch the option off, and **Run again** picks up the rest. ## What maps to what Categories are flattened. `MediaSpace>site>galleries>Engineering>1. Term>Electronics` becomes a category titled `Electronics` described as `Engineering: 1. Term: Electronics`, so its place in the Kaltura tree stays searchable. Titles appear in URLs, so a collision appends the parent name: `Electronics (Physics)`. Every category beneath a chosen one is created, including galleries holding nothing. To rebuild one, delete the MediaCMS category and its row in Migration Records, then re-import. ### Roles The **Global role mapping** tab lists the roles your portal actually has, fetched live from Kaltura, next to what each one should become in MediaCMS. Kaltura's own internal roles are hidden: a role tagged `kmc` or `kms` is always offered, an untagged one is judged by its system name. The defaults are: | Kaltura role | MediaCMS | | --- | --- | | Content Moderator (KMC) | Editor | | Content Uploader (KMC), Player Designer (KMC) | Manager | | Manager (KMC), Publisher Administrator (KMC) | Super user and staff | | `adminRole` | Advanced user | | `unmoderatedAdminRole` | Editor | | `viewerRole`, `privateOnlyRole`, `unconfirmedViewerRole` | Plain user | | anything not listed | Plain user | The `(KMC)` label marks the rows that are roles defined in the Kaltura Management Console. It is only a label: a portal reports the bare name, and the match ignores anything in trailing brackets, so `Manager (KMC)` still matches a role the portal calls `Manager`. **Where the role is read from.** A console role first: `user.get` returns `roleIds`, and the first is looked up in `userRole.list` for its `systemName`, or its `name`. A user with no console role gets their MediaSpace role instead, read from the KMS user schema metadata: ``` metadata_metadata.list filter:metadataObjectTypeEqual=3 filter:objectIdEqual= → adminRole ``` Only the object type is pinned. Adding `metadataProfileIdEqual` returns nothing, and the profile differs per KMS instance anyway. **The five lowercase rows** are MediaSpace's own roles, the ones its admin offers on every instance. They are a fixed list rather than something a portal defines, so they are listed rather than fetched and **Load roles from the source** leaves them on the table. Rows are matched on the role id, which is exact within a partner, and fall back to the name. Change any of them before you start; the mapping is saved with the migration, so two migrations from two portals can map the same role name differently. A role is applied **only to accounts the migration creates**. A reused account keeps whatever permissions it already had, so a re-run never re-grants or resets a role on a real person. Note that **Super user and staff** really does mean Django superuser: check the table before starting rather than after. ### Permissions Kaltura entries carry no public or private flag of their own. What decides visibility is the categories an entry is published through, and MediaCMS derives its own state from those. The category type is not a field either: it comes out of `privacy`, `appearInList` and `contributionPolicy` read together, so a channel *named* Restricted may well be configured as Public. The fields decide, not the name. Two of those fields restrict a category, and they restrict different things. `privacy` ("Content Privacy") says who may view the content. `appearInList` ("Category Listing") says who may see the category at all, which KMS describes as *visible only to users with specific permissions to access this category content*. Either one being set to anything other than "No Restriction" means Kaltura has named the people who belong there, so the category becomes an RBAC category and those people become its group. The media state is a separate question, answered by `privacy` alone. | Content Privacy | Category Listing | MediaCMS category | Media state | | --- | --- | --- | --- | | No Restriction | No Restriction | plain | public | | No Restriction | Private | RBAC category with a group | public | | Requires Authentication | No Restriction | RBAC category with a group | private, visible to the group | | Requires Authentication | Private | RBAC category with a group | private | | Private | either | RBAC category with a group | private, visible to the group | | Shared Repository | either | RBAC category with a group | private, visible to the group | `contributionPolicy` ("Who can add content") is a publishing right, not a visibility one, so it never makes a category less visible than its privacy says: KMS's "Public, Restricted" means anyone may watch while only members may contribute, and that arrives public. An entry in more than one category takes the most permissive of them, which is how Kaltura behaves too. An entry in no category at all is private. KMS marks an entry as Unlisted by placing it in its `>unlisted` category, which is open to everyone, so that category is read by what it marks rather than by its privacy: it counts as an unlisted category. An entry in it arrives **unlisted**, unless it is also published to a public gallery or channel, in which case it is **public**, as it is to anyone browsing that gallery on Kaltura. `>private` is read the same way but overrides everything: an entry in it is private. The `archive` and `playlists` categories do not count at all. Two things override all of this: `displayInSearch` set to none downgrades public to unlisted, and an entry that has not cleared moderation is private wherever it sits, because it was not published on the source either. An entry still awaiting approval inside a moderated category does not inherit that category's state. Two things about this mapping are worth knowing before you run it. **Open becomes private and shared with the channel's group.** Open in Kaltura means any logged in user may watch. MediaCMS has no such state, so the entry arrives private in an RBAC category, and the people Kaltura named on the channel see it through its group. MediaCMS shows such an entry as **Shared**. Widening it to everyone is then the owner's decision. **Requires Authentication becomes a group holding the people Kaltura named**, the owner among them, rather than a plain category. MediaCMS has no "any logged in user may view" setting for a category, so the alternatives were to drop the restriction or to hand it to somebody. It is handed to the owner, who can widen it from the group in the admin; a plain category would have been the same decision made silently, and made wrong. A category gets a group only when `USE_RBAC = True`. For each one the migration creates an RBAC group, marks the MediaCMS category as an RBAC category and attaches the group to it, then adds the members Kaltura reports: | Kaltura permission level | MediaCMS role | | --- | --- | | Manager | manager | | Moderator | manager | | Contributor | contributor | | Member | member | Kaltura's fifth level, `NONE`, is not a membership and is dropped. The category owner is added as a manager whether or not Kaltura lists them as a member, and a membership request that is still pending is not access, so it is not carried over. Members get accounts even when **Migrate all users** is off, because a membership pointing at no account is worth nothing. **Users are attached to the group, not to the category.** MediaCMS has no user to category link at all. The chain is `RBACMembership(user, group, role)`, then the group's `categories`, then the category, and every access check joins across it. So a member reaches the media one hop away, through the group. For the three category types that get no group, no user is associated with anything: the only user on that media is its owner. What each role then allows inside that category: | MediaCMS role | What it allows | | --- | --- | | member | View every media in the category, whatever state the media itself is in. | | contributor | The above, plus editing media and putting new media into the category. | | manager | The above, plus owner level access to **every** media in the category, not only their own. | This is why the media can stay `private` and still be watchable by the right people. The state is not what opens it, the group is: a member passes `has_member_access_to_media` and a stranger does not. An RBAC category is also hidden from the public category listing, so it does not advertise itself to people outside the group. The group is keyed on the source installation and the category id together, so a re-run, or a second migration of the same portal, finds the group that already exists instead of building a parallel one. Memberships are added, never changed or removed. If someone's Kaltura permission level changes and you run the migration again, the MediaCMS membership stays as it was, on purpose: access granted or adjusted inside MediaCMS is not the migration's to overwrite on a later run. Change it in the admin, or delete the membership and re-import. With `USE_RBAC = False` the private categories are still created and their media still land private, so nothing is exposed. It is only the member lists that have nowhere to go, and the migration log says so. **Shared Repository needs no special handling.** It is a channel type like the others, and its viewing rule is members only, so it arrives as a members-only category and lands on private with a group, exactly like a Private channel. What does not come across is the thing that makes it a repository: content in it may be published onward into other channels, and that publishing entitlement has no MediaCMS equivalent. The content itself is unaffected. An entry that was already published into other channels belongs to those categories too, and takes the most permissive state among them, so a repository item published to a public channel still arrives public. **Note on the two Public variants.** `contributionPolicy` says who may add content, not who may watch it, so it never makes a category less visible than its privacy allows. This matters for the type KMS calls Public, Restricted: anyone including anonymous users may watch it while only members may contribute. It maps to public, the same as Public, Open. Only in the authenticated tier does the members-only setting narrow things, because there the difference between a listed channel and a members-only one is a real access difference. ## Skipping transcoding With this on, MediaCMS re-encodes nothing. The source flavor becomes the original file and each transcoded flavor is attached as an `Encoding` on the closest **active** MediaCMS profile. HLS is packaged once per media afterwards, as for a normal upload. If the account has purged its source flavors, the tallest available one is used instead and this is logged. The small preview that plays when you hover a video is made by MediaCMS as one of its encoding profiles, so skipping transcoding would skip it too. It is asked for on its own after each import instead, and an imported video gets a preview like any other. Sprites were never affected. **Kaltura flavors are the same idea as MediaCMS encoding profiles**, the set of transcoded versions a video is offered in, and this option only goes as well as the two ladders line up. So check `/admin/files/encodeprofile/` first: * The profiles you want must be **active**. Inactive ones are never considered, so a portal with 1080p disabled gains no 1080p encodings, and a flavor with no active profile near its size is skipped with that resolution simply missing. * Line the profiles up with the flavors your Kaltura produces. A flavor goes to the closest active profile by height, and only when the two are within a size ratio of each other, so a 576 tall flavor lands on the 480 profile while nothing lands far from its real size. * Kaltura ladders are often not the round numbers MediaCMS ships with. Real ones include 480x272 and 1024x576. **Adding a brand new encoding profile takes real work.** Creating it in the Django admin is the easy half, the video player also has to know about the resolution, so budget for a code change rather than a form entry. **The original is registered as an encoding too**, on whichever profile matches its height, unless a flavor already holds that profile. A media file is not a quality the player offers, so without this the original's resolution is missing from the menu and a source whose ladder stops well below it is only ever watchable small. The file is reused rather than re-encoded, which costs a second copy on disk. An original far above the tallest active profile is not registered: labelling a 2160p file as 1080p would make the player lie about it. Images have no flavors in Kaltura, so their file comes from the entry itself. ### Multi-stream recordings Kaltura Capture records the camera and each screen as **separate entries** joined by `parentEntryId`. A listing never returns the children, so without asking for them by name the other streams are simply lost. Each entry is asked, one `baseEntry.list` call apiece. When children are found, the streams are drawn side by side in one video on a 16:9 canvas, each scaled to fit its box: | Streams | Layout | | --- | --- | | 2 | The child large on the left, the parent small on the right. | | 3 | The children stacked on the left, the parent small in the middle of the right. | MediaCMS plays one file per media, so one picture is what the recording becomes. Every stream is **also** imported as a media of its own, titled " (stream 1)", "(stream 2)" and so on, with the parent's camera as stream 1. They keep the recording's owner, state and date but none of its categories, so a gallery shows each recording once. **Combining always re-encodes.** There is no stream copy that merges pictures, so the work has to happen somewhere, and **Skip transcoding** decides where: | Skip transcoding | What happens | Ladder | | --- | --- | --- | | on | Every rung of the biggest screen's ladder is drawn and attached as an encoding. MediaCMS transcodes nothing. | whatever Kaltura had | | off | Only the originals are drawn, and MediaCMS builds its own ladder from that. | the portal's full ladder | Every other stream is fetched **once** whatever the setting: one flavor wide enough for its box serves every rung. If the streams cannot be drawn together, the entry arrives as its own stream alone and the children still arrive as media of their own. Two limits worth knowing. Up to three streams are combined; a fourth is logged and left out of the picture, though it still arrives as a media of its own. And the audio is taken from one stream only, the parent's where it has any: every stream carries the same microphone, so mixing them would play everything twice. ### Quiet hours **Do not run between** holds the migration inside a window of your choosing and lets it carry on by itself afterwards. Give it a start and an end in the portal's timezone, the same clock the scheduled start uses. An end earlier than the start runs over midnight, so `22:00` to `06:00` is a night window. Nothing is paused. The migration stays running and simply does no work while it is inside the window, looking again every five minutes, so it picks up on its own within five minutes of the window closing. That matters: it needs no timer held anywhere, which is what a schedule spanning weeks cannot rely on. **Run unpaused during weekends** exempts Saturday and Sunday from that window, for a portal whose quiet hours only mean anything on a working day. Two things worth knowing. A page already in flight is finished rather than abandoned, so the window takes effect at the next page and a large download in progress will run past the boundary. And a window that says nothing lets the migration run: switched off, either end unreadable, or both ends the same. A typo that quietly halts a month long run would be far worse than one that fails to hold it back, so the form refuses a bad window outright rather than storing one that does nothing. ### A scheduled start on a finished migration Scheduling a migration that has already completed arms a **re-run** at that time, the same sweep the **Run again** button does: what is imported is skipped, failures are retried and anything new at the source is picked up. The status stays **Completed** until it fires, and the row on the migrations list says when it is due. ## Pausing, resuming and running again Media are handled ten at a time, so a pause takes effect within a few items. * **Pause** stops after the items in flight and keeps the resume position. * **Resume** continues from there. * **Abort** stops for good, keeping what was imported and the position, so it can still be restarted. * **Run again**, on a finished migration, sweeps the source once more. Media are walked in creation order rather than by page number, because Kaltura refuses to page past ten thousand rows. The migration remembers the last creation time it reached and the ids already handled at that exact time, and that position is the resume point, which is why pause and resume land in the right place. Nothing is imported twice. Every migrated object is written to the mapping table, and each item checks it before anything is downloaded, both for this migration and for any other pointing at the same source. Skipping costs one database lookup and no call to Kaltura. Failed items are the exception and are retried, along with anything added at the source since. ## Monitoring `/migrations/` shows counts per object type, a live log refreshing every five seconds, and the ID mapping table of source ID, MediaCMS ID, status and time. The same rows are in the Django admin under Migration Records, where they can be searched and exported. Because the mapping is stored rather than derived, a run can be watched live, verified afterwards and rerun safely. ## Troubleshooting **"No fallback owner: name one, or give the portal an administrator".** Kaltura has no fallback owner option. A media whose owner cannot be resolved goes to whoever started the migration, then to `admin`, then to any superuser, so this only appears on a portal with none of those. **Media imported but not visible.** Check the publish state option and the entry's Kaltura categories. Entries in no category become private by design. **Encodings missing.** The portal's active encoding profiles decide what can be attached. Check `/admin/files/encodeprofile/`. **Stuck in running.** Look at Last activity on the list page. If it has gone stale the usual cause is a worker that died mid page, leaving the migration marked running with nothing queued. Pause, then Resume: replaying a page is safe because every importer checks the mapping table before writing.