如何通过Tin Can API跟踪PDF阅读并插入代码发送xAPI Statement至LRS?
Great question! You’re absolutely right that Tin Can API (now widely known as xAPI) excels at tracking nearly every type of learning activity—including PDF reading. And yes, you can absolutely use JavaScript or C# to send xAPI statements directly to a Learning Record Store (LRS). Let’s walk through how to implement this, with practical examples for both languages.
Before diving into code, remember that every xAPI statement needs three mandatory components:
- Actor: The person performing the activity (e.g., a user’s email or unique ID)
- Verb: The action taken (must use a standard xAPI Verb IRI, like
http://adlnet.gov/expapi/verbs/viewedorhttp://adlnet.gov/expapi/verbs/completed) - Object: The activity being tracked (your PDF, identified by a unique URL/IRI, with a type of
http://adlnet.gov/expapi/activities/resource)
All statements are sent as JSON via a POST request to your LRS’s /statements endpoint, usually with Basic Authentication.
For PDFs embedded in a web page, JavaScript is the go-to choice. We’ll use Mozilla’s pdf.js library to render the PDF and hook into reading events (like page turns or scroll progress).
Step 1: Set Up PDF.js
First, include the PDF.js library in your page:
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
Step 2: Render the PDF & Track Events
Here’s a complete example that tracks when a user navigates to a new page and sends an xAPI statement:
// Configuration const pdfUrl = "https://your-domain.com/path/to/your/document.pdf"; const lrsEndpoint = "https://your-lrs-url.com/xapi/statements"; const lrsUsername = "your-lrs-username"; const lrsPassword = "your-lrs-password"; // Initialize PDF.js const pdfjsLib = window['pdfjs-dist/build/pdf']; pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; async function renderPDF() { const pdf = await pdfjsLib.getDocument(pdfUrl).promise; const pageContainer = document.getElementById('pdf-container'); // Render first page let currentPage = 1; await renderPage(currentPage); // Track page changes (e.g., when user clicks next/prev or scrolls) document.getElementById('next-page').addEventListener('click', async () => { if (currentPage < pdf.numPages) { currentPage++; await renderPage(currentPage); sendXapiStatement(`viewed page ${currentPage} of ${pdf.numPages}`); } }); document.getElementById('prev-page').addEventListener('click', async () => { if (currentPage > 1) { currentPage--; await renderPage(currentPage); sendXapiStatement(`viewed page ${currentPage} of ${pdf.numPages}`); } }); } // Helper to render a single page async function renderPage(pageNum) { const page = await pdf.getPage(pageNum); const viewport = page.getViewport({ scale: 1.5 }); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.height = viewport.height; canvas.width = viewport.width; const pageContainer = document.getElementById('pdf-container'); pageContainer.innerHTML = ''; pageContainer.appendChild(canvas); await page.render({ canvasContext: context, viewport: viewport }).promise; } // Helper to send xAPI statement to LRS async function sendXapiStatement(activityDescription) { const statement = { actor: { mbox: "mailto:user@your-domain.com", // Replace with user's actual email/ID name: "John Doe" }, verb: { id: "http://adlnet.gov/expapi/verbs/viewed", display: { "en-US": "viewed" } }, object: { id: pdfUrl, definition: { name: { "en-US": "Your PDF Document Title" }, description: { "en-US": activityDescription }, type: "http://adlnet.gov/expapi/activities/resource" } } }; // Encode credentials for Basic Auth const credentials = btoa(`${lrsUsername}:${lrsPassword}`); try { const response = await fetch(lrsEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${credentials}` }, body: JSON.stringify(statement) }); if (!response.ok) { console.error('Failed to send xAPI statement:', await response.text()); } else { console.log('xAPI statement sent successfully!'); } } catch (error) { console.error('Error sending xAPI statement:', error); } } // Start rendering renderPDF();
Bonus: Track Completion
To track when a user finishes the PDF, add a check when they reach the last page, and send a statement with the completed verb (http://adlnet.gov/expapi/verbs/completed).
If you’re building a desktop app (e.g., WPF, WinForms) or need to send statements from a backend service, C# works perfectly. We’ll use HttpClient to send the POST request.
Example Code
using System; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; // Use Newtonsoft.Json or System.Text.Json namespace XapiPdfTracker { class Program { static async Task Main(string[] args) { // Configuration string lrsEndpoint = "https://your-lrs-url.com/xapi/statements"; string lrsUsername = "your-lrs-username"; string lrsPassword = "your-lrs-password"; string pdfUrl = "https://your-domain.com/path/to/your/document.pdf"; // Create xAPI statement var statement = new { actor = new { mbox = "mailto:user@your-domain.com", name = "John Doe" }, verb = new { id = "http://adlnet.gov/expapi/verbs/viewed", display = new { en_US = "viewed" } }, @object = new // "object" is a keyword in C#, so use @ { id = pdfUrl, definition = new { name = new { en_US = "Your PDF Document Title" }, description = new { en_US = "Completed reading the entire PDF" }, type = "http://adlnet.gov/expapi/activities/resource" } } }; // Convert statement to JSON string jsonStatement = JsonConvert.SerializeObject(statement); // Set up HttpClient with Basic Auth using (var client = new HttpClient()) { var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{lrsUsername}:{lrsPassword}")); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); // Send POST request var content = new StringContent(jsonStatement, Encoding.UTF8, "application/json"); var response = await client.PostAsync(lrsEndpoint, content); if (response.IsSuccessStatusCode) { Console.WriteLine("xAPI statement sent successfully!"); } else { Console.WriteLine($"Failed to send statement: {response.StatusCode}"); Console.WriteLine(await response.Content.ReadAsStringAsync()); } } } } }
- LRS Authentication: Almost all LRS systems require Basic Authentication—make sure your credentials are correctly encoded.
- Verb & Object Standards: Stick to official xAPI Verb IRIs and use the correct activity type for PDFs to ensure compatibility across systems.
- Event Throttling: Don’t send a statement for every tiny action (e.g., every scroll pixel). Instead, trigger statements on meaningful events: page turns, completion, or spending X seconds on a page.
- Error Handling: Always handle failed requests—log errors, retry if appropriate, and inform the user if needed.
内容的提问来源于stack exchange,提问作者Hendri Triwanto




